<!DOCTYPE html>


<html lang="zh-CN">


<head>
  <meta charset="utf-8" />
    
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
  <title>
     
  </title>
  <meta name="generator" content="hexo-theme-ayer">
  
  <link rel="shortcut icon" href="/favicon.ico" />
  
  
<link rel="stylesheet" href="/dist/main.css">

  
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/Shen-Yu/cdn/css/remixicon.min.css">

  
<link rel="stylesheet" href="/css/custom.css">

  
  
<script src="https://cdn.jsdelivr.net/npm/pace-js@1.0.2/pace.min.js"></script>

  
  

  

</head>

</html>

<body>
  <div id="app">
    
      
    <main class="content on">
      
<section class="cover">
    
      
      <a class="forkMe" href="https://github.com/Shen-Yu/hexo-theme-ayer"
        target="_blank"><img width="149" height="149" src="/images/forkme.png"
          class="attachment-full size-full" alt="Fork me on GitHub" data-recalc-dims="1"></a>
    
  <div class="cover-frame">
    <div class="bg-box">
      <img src="/images/cover1.jpg" alt="image frame" />
    </div>
    <div class="cover-inner text-center text-white">
      <h1><a href="/">Hexo</a></h1>
      <div id="subtitle-box">
        
        <span id="subtitle"></span>
        
      </div>
      <div>
        
      </div>
    </div>
  </div>
  <div class="cover-learn-more">
    <a href="javascript:void(0)" class="anchor"><i class="ri-arrow-down-line"></i></a>
  </div>
</section>



<script src="https://cdn.jsdelivr.net/npm/typed.js@2.0.11/lib/typed.min.js"></script>


<!-- Subtitle -->

  <script>
    try {
      var typed = new Typed("#subtitle", {
        strings: ['面朝大海，春暖花开', '何来天才，唯有苦练', '集中一点，登峰造极'],
        startDelay: 0,
        typeSpeed: 200,
        loop: true,
        backSpeed: 100,
        showCursor: true
      });
    } catch (err) {
      console.log(err)
    }
  </script>
  
<div id="main">
  <section class="outer">
  
  

<div class="notice" style="margin-top:50px">
    <i class="ri-heart-fill"></i>
    <div class="notice-content" id="broad"></div>
</div>
<script type="text/javascript">
    fetch('https://v1.hitokoto.cn')
        .then(response => response.json())
        .then(data => {
            document.getElementById("broad").innerHTML = data.hitokoto;
        })
        .catch(console.error)
</script>

<style>
    .notice {
        padding: 20px;
        border: 1px dashed #e6e6e6;
        color: #969696;
        position: relative;
        display: inline-block;
        width: 100%;
        background: #fbfbfb50;
        border-radius: 10px;
    }

    .notice i {
        float: left;
        color: #999;
        font-size: 16px;
        padding-right: 10px;
        vertical-align: middle;
        margin-top: -2px;
    }

    .notice-content {
        display: initial;
        vertical-align: middle;
    }
</style>
  
  <article class="articles">
    
    
    
    
    <article
  id="post-develop/基于Spring Cloud微服务化开发平台Cloud-Platform完整解析"
  class="article article-type-post"
  itemscope
  itemprop="blogPost"
  data-scroll-reveal
>
  <div class="article-inner">
    
    <header class="article-header">
       
<h2 itemprop="name">
  <a class="article-title" href="/2020/11/11/develop/%E5%9F%BA%E4%BA%8ESpring%20Cloud%E5%BE%AE%E6%9C%8D%E5%8A%A1%E5%8C%96%E5%BC%80%E5%8F%91%E5%B9%B3%E5%8F%B0Cloud-Platform%E5%AE%8C%E6%95%B4%E8%A7%A3%E6%9E%90/"
    >基于Spring Cloud微服务化开发平台Cloud-Platform完整解析.md</a> 
</h2>
 

    </header>
     
    <div class="article-meta">
      <a href="/2020/11/11/develop/%E5%9F%BA%E4%BA%8ESpring%20Cloud%E5%BE%AE%E6%9C%8D%E5%8A%A1%E5%8C%96%E5%BC%80%E5%8F%91%E5%B9%B3%E5%8F%B0Cloud-Platform%E5%AE%8C%E6%95%B4%E8%A7%A3%E6%9E%90/" class="article-date">
  <time datetime="2020-11-10T16:00:00.000Z" itemprop="datePublished">2020-11-11</time>
</a> 
  <div class="article-category">
    <a class="article-category-link" href="/categories/develop/">develop</a>
  </div>
   
    </div>
      
    <div class="article-entry" itemprop="articleBody">
       
  <h4 id="小Hub领读："><a href="#小Hub领读：" class="headerlink" title="小Hub领读："></a>小Hub领读：</h4><p>讲解视频也同步发布啦，记得去看哈，一键三连哇。</p>
<p>视频讲解：<a target="_blank" rel="noopener" href="https://www.bilibili.com/video/bv1SD4y1o7cN">https://www.bilibili.com/video/bv1SD4y1o7cN</a></p>
<p>这系统，一定要学会用户-服务认证，服务-服务鉴权的那一套，这才算学会。</p>
<hr>
<h2 id="简介"><a href="#简介" class="headerlink" title="简介"></a>简介</h2><p>Cloud-Platform是国内首个基于Spring Cloud微服务化开发平台，具有统一授权、认证后台管理系统，其中包含具备用户管理、资源权限管理、网关API 管理等多个模块，支持多业务系统并行开发，可以作为后端服务的开发脚手架。代码简洁，架构清晰，适合学习和直接项目中使用。核心技术采用Spring Boot 2.1.2以及Spring Cloud (Greenwich.RELEASE) 相关核心组件，采用Nacos注册和配置中心，集成流量卫兵Sentinel，前端采用vue-element-admin组件，Elastic Search自行集成。</p>
<p>B站视频讲解：<a target="_blank" rel="noopener" href="https://www.bilibili.com/video/BV1SD4y1o7cN/">https://www.bilibili.com/video/BV1SD4y1o7cN/</a></p>
<p><strong>ps：注意本文讲解是基于Cloud-Platform v2.5版本，不是最新版本！</strong></p>
<h2 id="技术选型"><a href="#技术选型" class="headerlink" title="技术选型"></a>技术选型</h2><p>前端：vue-element-admin</p>
<p>后端：springcloud（eureka、gateway、admin、sidecar、Hystrix、feign、ribbon、zipkin）、tk+mybatis、lucene、jwt、rest</p>
<h2 id="项目结构"><a href="#项目结构" class="headerlink" title="项目结构"></a>项目结构</h2><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">ace-security</span><br><span class="line">  ace-modules--------------公共服务模块（基础系统、搜索、OSS）</span><br><span class="line">  ace-auth-----------------服务鉴权中心</span><br><span class="line">  ace-gate-----------------网关负载中心</span><br><span class="line">  ace-common---------------通用脚手架</span><br><span class="line">  ace-control--------------运维中心（监控、链路）</span><br><span class="line">  ace-sidebar--------------调用第三方语言服务</span><br></pre></td></tr></table></figure>

<h2 id="项目启动"><a href="#项目启动" class="headerlink" title="项目启动"></a>项目启动</h2><p><strong>须知：</strong> 因为Cloud-Platform是一个前后端分离的项目，所以后端的服务必须先启动，在后端服务启动完成后，再启动前端的工程。</p>
<h3 id="环境"><a href="#环境" class="headerlink" title="环境"></a>环境</h3><ul>
<li>mysql，redis，maven</li>
<li>jdk1.8</li>
<li>IDE插件一个，lombok插件，具体百度即可</li>
<li>node</li>
</ul>
<h3 id="前端"><a href="#前端" class="headerlink" title="前端"></a>前端</h3><p>git链接：<a target="_blank" rel="noopener" href="https://gitee.com/geek_qi/cloud-platform-ui">https://gitee.com/geek_qi/cloud-platform-ui</a></p>
<ul>
<li>先clone到本地，并进入cloud-platform-ui目录打开命令行窗口（cmd）</li>
<li>因为涉及node.js，所以需要安装npm，node等环境</li>
</ul>
<p>node.js安装教程：<a target="_blank" rel="noopener" href="http://nodejs.cn/download/%E4%B8%8B%E8%BD%BDmsi%E7%89%88%E6%9C%AC%E5%AE%89%E8%A3%85%E3%80%82">http://nodejs.cn/download/下载msi版本安装。</a></p>
<p>安装之后，命令行窗口，表示安装成功。</p>
<p><img src="http://iubest.gitee.io/pic/640-1601000484396.webp" alt="img"></p>
<p>给项目打包依赖</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"># 1、安装淘宝镜像依赖</span><br><span class="line">npm install -g cnpm --registry&#x3D;https:&#x2F;&#x2F;registry.npm.taobao.org</span><br><span class="line"># 2、安装项目依赖</span><br><span class="line">cnpm install</span><br><span class="line"># 启动服务</span><br><span class="line">npm run dev</span><br></pre></td></tr></table></figure>

<p>启动成功后会自动打开链接：<a target="_blank" rel="noopener" href="http://localhost:9527/">http://localhost:9527/</a></p>
<h3 id="后端"><a href="#后端" class="headerlink" title="后端"></a>后端</h3><p>首先clone项目下来（v2.5版本）：<a target="_blank" rel="noopener" href="https://gitee.com/geek_qi/cloud-platform/tree/v2.5/">https://gitee.com/geek_qi/cloud-platform/tree/v2.5/</a></p>
<p>导入到idea中，然后导入数据库sql：</p>
<p><img src="http://iubest.gitee.io/pic/640-1601000484471.webp" alt="img"></p>
<p>修改数据库的账号密码（直接ctrl+shirt+r，搜索datasource:，可以很方便修改）：</p>
<p><img src="http://iubest.gitee.io/pic/640-1601000484443.webp" alt="img"></p>
<p>接下来启动redis，然后安装顺序启动我们的服务</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"># 启动顺序</span><br><span class="line">CenterBootstrap -&gt; AuthBootstrap -&gt; AdminBootstrap -&gt; GatewayServerBootstrap</span><br></pre></td></tr></table></figure>

<p>其他监控或服务可以先不启动。</p>
<h2 id="服务说明"><a href="#服务说明" class="headerlink" title="服务说明"></a>服务说明</h2><h3 id="ace-auth-server"><a href="#ace-auth-server" class="headerlink" title="ace-auth-server"></a>ace-auth-server</h3><p>关键类：</p>
<ul>
<li><p>AuthController</p>
</li>
<li><ul>
<li>/jwt开头的控制器，登录、刷新、校验jwt</li>
<li>网关不拦截这个链接的请求</li>
</ul>
</li>
<li><p>ClientController</p>
</li>
<li><ul>
<li>对外暴露的接口，可通过客户端的id和密钥获取到对应的授权相关资源</li>
</ul>
</li>
<li><p>ServiceController</p>
</li>
<li><ul>
<li>后台管理系统服务管理模块接口</li>
</ul>
</li>
<li><p>ClientTokenInterceptor</p>
</li>
<li><ul>
<li>拦截feign接口发起的请求，并自动添加请求头token</li>
</ul>
</li>
<li><p>ServiceAuthRestInterceptor</p>
</li>
<li><ul>
<li>拦截的url为/service/**（WebConfiguration）</li>
<li>服务之间的调用鉴权</li>
<li>一般feign、或者restTemplate方式调用</li>
</ul>
</li>
<li><p>UserAuthRestInterceptor</p>
</li>
<li><ul>
<li>拦截的url为/service/**（WebConfiguration）</li>
<li>获取用户的token并解析，为会话上下文添加用户信息（ThreadLocal）</li>
</ul>
</li>
<li><p>OkHttpTokenInterceptor</p>
</li>
<li><ul>
<li>拦截所有的feign请求，OkHttp重写请求</li>
<li>OkHttp3一个强有力的机制，能够监控，重写以及重试（请求的）调用</li>
</ul>
</li>
</ul>
<h2 id="模块分析"><a href="#模块分析" class="headerlink" title="模块分析"></a>模块分析</h2><p><img src="http://iubest.gitee.io/pic/640-1601000484398.webp" alt="img"></p>
<h3 id="用户授权"><a href="#用户授权" class="headerlink" title="用户授权"></a>用户授权</h3><p>首先我们来弄清楚一下登录的流程，登录中和之后发生了什么事，那么我们打开前段登录页面，按下F12，然后点击登录，可以查看到以下几个动作：</p>
<ul>
<li>点击登录，提交表单：<a target="_blank" rel="noopener" href="http://localhost:9527/api/auth/jwt/token">http://localhost:9527/api/auth/jwt/token</a></li>
</ul>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">#返回值</span><br><span class="line">&#123;&quot;status&quot;:200,&quot;data&quot;:&quot;eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJhZG1pbiIsInVzZXJJZCI6IjEiLCJuYW1lIjoiTXIuQUciLCJleHAiOjE1NjE0NDEyNTZ9.tXNw8nhAFmI4QIQDpKy4DzWtJSTpfwD4685JqbA2pGScdyfXt_5DDs_r1gVZA4CwQC4oZxBsmLKZGclTLGc4HKeXlP2PiVoHZfSWymFRLNfvFqOzKUETJ6WpyDqK55yjf1wddTBD3VzSFvY49uunvozEcb2oFjOs3M_I2sgxAAU&quot;,&quot;rel&quot;:false&#125;</span><br></pre></td></tr></table></figure>

<p>可以看到登录成功之后返回的是一个jwt的token值，应该就是标识用户身份用的token。</p>
<ul>
<li>ajax发起请求获取用户信息以及菜单列表等：<a target="_blank" rel="noopener" href="http://localhost:9527/api/admin/user/front/info?token=eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJhZG1pbiIsInVzZXJJZCI6IjEiLCJuYW1lIjoiTXIuQUciLCJleHAiOjE1NjE0NDEyNTZ9.tXNw8nhAFmI4QIQDpKy4DzWtJSTpfwD4685JqbA2pGScdyfXt_5DDs_r1gVZA4CwQC4oZxBsmLKZGclTLGc4HKeXlP2PiVoHZfSWymFRLNfvFqOzKUETJ6WpyDqK55yjf1wddTBD3VzSFvY49uunvozEcb2oFjOs3M_I2sgxAAU">http://localhost:9527/api/admin/user/front/info?token=eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJhZG1pbiIsInVzZXJJZCI6IjEiLCJuYW1lIjoiTXIuQUciLCJleHAiOjE1NjE0NDEyNTZ9.tXNw8nhAFmI4QIQDpKy4DzWtJSTpfwD4685JqbA2pGScdyfXt_5DDs_r1gVZA4CwQC4oZxBsmLKZGclTLGc4HKeXlP2PiVoHZfSWymFRLNfvFqOzKUETJ6WpyDqK55yjf1wddTBD3VzSFvY49uunvozEcb2oFjOs3M_I2sgxAAU</a></li>
</ul>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"># 返回值</span><br><span class="line">&#123;&quot;id&quot;:&quot;1&quot;,&quot;username&quot;:&quot;admin&quot;,&quot;name&quot;:&quot;Mr.AG&quot;,&quot;description&quot;:&quot;&quot;,&quot;menus&quot;:[&#123;&quot;code&quot;:&quot;userManager&quot;,&quot;type&quot;:&quot;menu&quot;,&quot;uri&quot;:&quot;&#x2F;admin&#x2F;user&quot;,&quot;method&quot;:&quot;GET&quot;,&quot;name&quot;:&quot;访问&quot;,&quot;menu&quot;:&quot;用户管理&quot;&#125;,&#123;&quot;code&quot;:&quot;baseManager&quot;,&quot;type&quot;:&quot;menu&quot;,&quot;uri&quot;:&quot;&#x2F;admin&quot;,&quot;method&quot;:&quot;GET&quot;,&quot;name&quot;:&quot;访问&quot;,&quot;menu&quot;:&quot;基础配置管理&quot;&#125;,</span><br><span class="line">.....(此次删除了部分),&#123;&quot;code&quot;:&quot;serviceManager:btn_clientManager&quot;,&quot;type&quot;:&quot;button&quot;,&quot;uri&quot;:&quot;&#x2F;auth&#x2F;service&#x2F;&#123;*&#125;&#x2F;client&quot;,&quot;method&quot;:&quot;POST&quot;,&quot;name&quot;:&quot;服务授权&quot;,&quot;menu&quot;:&quot;服务管理&quot;&#125;]&#125;</span><br></pre></td></tr></table></figure>

<p>同时注意请求头的信息：带有刚才登录后的jwt token，名称叫Authorization，以后的所有请求都会带上这个Authorization用于标识用户身份。 <img src="http://iubest.gitee.io/pic/640-1601000484473.webp" alt="img"></p>
<p>接下来我们来走下这个过程：</p>
<ul>
<li><p>客户端点击按钮发起登录请求<a target="_blank" rel="noopener" href="http://localhost:9527/api/auth/jwt/token">http://localhost:9527/api/auth/jwt/token</a></p>
</li>
<li><ul>
<li>获取请求uri，method</li>
<li>判断是否是不拦截地址</li>
<li>刚好我们发现不拦截地址中有这个配置：gate: ignore: startWith: /auth/jwt</li>
<li>所以网关过滤器直接将请求代理到ace-auth服务</li>
</ul>
</li>
<li><ul>
<li>到达网关gate首先经过我们的过滤器AccessGatewayFilter</li>
</ul>
</li>
<li><p>网关ace-gateway-v2的代理以及过滤配置</p>
</li>
</ul>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"> # 网关代理规则</span><br><span class="line"> spring:</span><br><span class="line">   cloud:</span><br><span class="line">      gateway:</span><br><span class="line">        locator:</span><br><span class="line">          enabled: true</span><br><span class="line">        routes:</span><br><span class="line">        # &#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;</span><br><span class="line">        - id: ace-auth</span><br><span class="line">          uri: lb:&#x2F;&#x2F;ace-auth</span><br><span class="line">          order: 8000</span><br><span class="line">          predicates:</span><br><span class="line">          - Path&#x3D;&#x2F;api&#x2F;auth&#x2F;**</span><br><span class="line">          filters:</span><br><span class="line">          - StripPrefix&#x3D;2</span><br><span class="line">        - id: ace-admin</span><br><span class="line">          uri: lb:&#x2F;&#x2F;ace-admin</span><br><span class="line">          order: 8001</span><br><span class="line">          predicates:</span><br><span class="line">          - Path&#x3D;&#x2F;api&#x2F;admin&#x2F;**</span><br><span class="line">          filters:</span><br><span class="line">          - StripPrefix&#x3D;2</span><br><span class="line">gate:</span><br><span class="line">  ignore:</span><br><span class="line">    startWith: &#x2F;auth&#x2F;jwt</span><br></pre></td></tr></table></figure>

<p>那么我们进入ace-auth中找到对应controller 可以找到这里：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">@RestController</span><br><span class="line">@RequestMapping(&quot;jwt&quot;)</span><br><span class="line">@Slf4j</span><br><span class="line">public class AuthController &#123;    @RequestMapping(value &#x3D; &quot;token&quot;, method &#x3D; RequestMethod.POST)</span><br><span class="line">    public ObjectRestResponse&lt;String&gt; createAuthenticationToken(</span><br><span class="line">            @RequestBody JwtAuthenticationRequest authenticationRequest) throws Exception &#123;</span><br><span class="line">        log.info(authenticationRequest.getUsername()+&quot; require logging...&quot;);</span><br><span class="line">        final String token &#x3D; authService.login(authenticationRequest);</span><br><span class="line">        return new ObjectRestResponse&lt;&gt;().data(token);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>这个方法里面只有一个方法就是authService.login，点这个方法进去发现里面又有个方法是<strong>userService</strong>.validate，这是一个feign接口</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">@FeignClient(value &#x3D; &quot;ace-admin&quot;,configuration &#x3D; FeignConfiguration.class)</span><br><span class="line">public interface IUserService &#123;  @RequestMapping(value &#x3D; &quot;&#x2F;api&#x2F;user&#x2F;validate&quot;, method &#x3D; RequestMethod.POST)</span><br><span class="line">  public UserInfo validate(@RequestBody JwtAuthenticationRequest authenticationRequest);</span><br></pre></td></tr></table></figure>

<p>}可以看到这是远程调用，那么接下来，我们看看远程调用的过程，这设计到服务间的相互鉴权</p>
<h3 id="服务间鉴权"><a href="#服务间鉴权" class="headerlink" title="服务间鉴权"></a>服务间鉴权</h3><p>上面的<strong>userService</strong>.validate，类上有这样的注解</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">@FeignClient(value &#x3D; &quot;ace-admin&quot;,configuration &#x3D; FeignConfiguration.class)</span><br><span class="line">表示声明式调用ace-admin服务，接下来我们解析一下这个过程发生了什么事情，我们可以看到有个configuration &#x3D; FeignConfiguration.class，我们打开FeignConfiguration</span><br><span class="line">@Configuration</span><br><span class="line">public class FeignConfiguration &#123;</span><br><span class="line">    @Bean</span><br><span class="line">    ClientTokenInterceptor getClientTokenInterceptor()&#123;</span><br><span class="line">        return new ClientTokenInterceptor();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>再打开ClientTokenInterceptor ：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line">public class ClientTokenInterceptor implements RequestInterceptor &#123;</span><br><span class="line">    private Logger logger &#x3D; LoggerFactory.getLogger(ClientTokenInterceptor.class);</span><br><span class="line">    @Autowired</span><br><span class="line">    private ClientConfiguration clientConfiguration;</span><br><span class="line">    @Autowired</span><br><span class="line">    private AuthClientService authClientService;    @Override</span><br><span class="line">    public void apply(RequestTemplate requestTemplate) &#123;</span><br><span class="line">        logger.info(&quot;----&gt; 为feign调用添加token头&quot;);</span><br><span class="line">        try &#123;</span><br><span class="line">            requestTemplate.header(clientConfiguration.getClientTokenHeader(), authClientService.apply(clientConfiguration.getClientId(), clientConfiguration.getClientSecret()));</span><br><span class="line">        &#125; catch (Exception e) &#123;</span><br><span class="line">            e.printStackTrace();</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line">从上面的配置我们知道，RequestInterceptor 是feign接口下的包装拦截器，从代码里面看到，其实意思就是在feign发起远程调用时候往请求里添加请求头信息（clientToken），所以发起的请求头中就有了当前服务的身份token，接受端的服务器就能根据token辨别来源服务器的身份。</span><br></pre></td></tr></table></figure>

<p>接下来我们看看接收端怎么辨别的。ServiceAuthRestInterceptor</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line">public class ServiceAuthRestInterceptor extends HandlerInterceptorAdapter &#123;</span><br><span class="line">    private Logger logger &#x3D; LoggerFactory.getLogger(ServiceAuthRestInterceptor.class);    @Override</span><br><span class="line">    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception &#123;        logger.info(&quot;------&gt;判断服务A是否有权限访问当前服务B ~&quot;);</span><br><span class="line">        HandlerMethod handlerMethod &#x3D; (HandlerMethod) handler;</span><br><span class="line">        &#x2F;&#x2F; 配置该注解，说明不进行服务拦截</span><br><span class="line">        IgnoreClientToken annotation &#x3D; handlerMethod.getBeanType().getAnnotation(IgnoreClientToken.class);</span><br><span class="line">        if (annotation &#x3D;&#x3D; null) &#123;</span><br><span class="line">            annotation &#x3D; handlerMethod.getMethodAnnotation(IgnoreClientToken.class);</span><br><span class="line">        &#125;</span><br><span class="line">        if(annotation!&#x3D;null) &#123;</span><br><span class="line">            return super.preHandle(request, response, handler);</span><br><span class="line">        &#125;        String token &#x3D; request.getHeader(serviceAuthConfig.getTokenHeader());</span><br><span class="line">        IJWTInfo infoFromToken &#x3D; serviceAuthUtil.getInfoFromToken(token);</span><br><span class="line">        String uniqueName &#x3D; infoFromToken.getUniqueName();</span><br><span class="line">        for(String client:serviceAuthUtil.getAllowedClient())&#123; &#x2F;&#x2F;ace-auth、ace-gate</span><br><span class="line">            if(client.equals(uniqueName))&#123;</span><br><span class="line">                return super.preHandle(request, response, handler);</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">        throw new ClientForbiddenException(&quot;Client is Forbidden!&quot;);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>从代码看到首先看下有没IgnoreClientToken的注解，有的话就跳过。没有的话继续获取调用端服务器的token，然后再去获取当前服务器允许访问的lient（<strong>serviceAuthUtil</strong>.getAllowedClient()），然后匹配调用端的名称是否在允许的客户端内，如果允许就继续，不允许就forbidden。所以挺清晰的。那这套客户端和允许的访问的客户端这套关系是哪里维护的呢，其实有两张表</p>
<p><img src="http://iubest.gitee.io/pic/640-1601000484453.webp" alt="img"></p>
<ul>
<li>auth_client ：客户端的id和名称等</li>
<li>auth_client_service：服务端允许调用的客户端关联表</li>
</ul>
<p>从这张表中，我们就可以得出哪些允许被访问，哪些不能被访问了</p>
<h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>第一件事</p>
<ul>
<li>新建所有需要用到的模块，基本搭建好框架</li>
<li>做好增删改查的封装、接口规范</li>
<li>全局异常捕捉封装</li>
<li>单元测试</li>
<li>通用工具类</li>
</ul>
<p>第二件事</p>
<ul>
<li>决定服务的授权模式（jwt、oauth2等）</li>
</ul>
<p>第三件事</p>
<ul>
<li>服务内部鉴权</li>
<li>权限控制</li>
</ul>
<p>视频讲解：<a target="_blank" rel="noopener" href="https://www.bilibili.com/video/bv1SD4y1o7cN">https://www.bilibili.com/video/bv1SD4y1o7cN</a></p>
 
      <!-- reward -->
      
    </div>
    

    <!-- copyright -->
    
    <footer class="article-footer">
       
  <ul class="article-tag-list" itemprop="keywords"><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/develop/" rel="tag">develop</a></li></ul>

    </footer>
  </div>

    
 
   
</article>

    
    <article
  id="post-develop/开源推荐 CoDo开源一站式DevOps平台"
  class="article article-type-post"
  itemscope
  itemprop="blogPost"
  data-scroll-reveal
>
  <div class="article-inner">
    
    <header class="article-header">
       
<h2 itemprop="name">
  <a class="article-title" href="/2020/11/11/develop/%E5%BC%80%E6%BA%90%E6%8E%A8%E8%8D%90%20CoDo%E5%BC%80%E6%BA%90%E4%B8%80%E7%AB%99%E5%BC%8FDevOps%E5%B9%B3%E5%8F%B0/"
    >开源推荐 CoDo开源一站式DevOps平台.md</a> 
</h2>
 

    </header>
     
    <div class="article-meta">
      <a href="/2020/11/11/develop/%E5%BC%80%E6%BA%90%E6%8E%A8%E8%8D%90%20CoDo%E5%BC%80%E6%BA%90%E4%B8%80%E7%AB%99%E5%BC%8FDevOps%E5%B9%B3%E5%8F%B0/" class="article-date">
  <time datetime="2020-11-10T16:00:00.000Z" itemprop="datePublished">2020-11-11</time>
</a> 
  <div class="article-category">
    <a class="article-category-link" href="/categories/develop/">develop</a>
  </div>
   
    </div>
      
    <div class="article-entry" itemprop="articleBody">
       
  <h1 id="CoDo开源一站式DevOps平台"><a href="#CoDo开源一站式DevOps平台" class="headerlink" title="CoDo开源一站式DevOps平台"></a>CoDo开源一站式DevOps平台</h1><p>有幸参与到CoDo项目的开发，这是一个非常棒的一站式开源运维平台，分享给大家</p>
<h2 id="平台介绍"><a href="#平台介绍" class="headerlink" title="平台介绍"></a>平台介绍</h2><p>CODO是一款为用户提供企业多混合云、自动化运维、完全开源的云管理平台。</p>
<p>CODO前端基于Vue iview开发、为用户提供友好的操作界面，增强用户体验。</p>
<p>CODO后端基于Python Tornado开发，其优势为轻量、简洁清晰、异步非阻塞。</p>
<p>CODO开源多云管理平台将为用户提供多功能：ITSM、基于RBAC权限系统、Web Terminnal登陆日志审计、录像回放、强大的作业调度系统、CMDB、监控报警系统、DNS管理、配置中心等</p>
<h3 id="产品架构"><a href="#产品架构" class="headerlink" title="产品架构"></a>产品架构</h3><p><img src="http://iubest.gitee.io/pic/640-1601001789263.jpg" alt="img"></p>
<h3 id="产品功能"><a href="#产品功能" class="headerlink" title="产品功能"></a>产品功能</h3><p><img src="http://iubest.gitee.io/pic/640-1601001746446.webp" alt="img"></p>
<h3 id="模块说明"><a href="#模块说明" class="headerlink" title="模块说明"></a>模块说明</h3><ul>
<li>项目前端：基于Vue + Iview-Admin实现的一套后台管理系统</li>
<li>管理后端：基于Tornado实现，提供Restful风格的API，提供基于RBAC的完善权限管理，可对所有用户的操作进行审计</li>
<li>定时任务：基于Tornado实现，定时任务系统，完全兼容<code>Linux Crontab</code>语法，且支持到秒级</li>
<li>任务调度：基于Tornado实现，系统核心调度，可分布式扩展，自由编排任务，自由定义流程，支持多种触发，支持审批审核，支持操作干预</li>
<li>资产管理：基于Tornado实现，资产管理系统，支持手动添加资产，同时也支持从AWS/阿里云/腾讯云自动获取资产信息</li>
<li>配置中心：基于Tornado实现，可基于不同项目、环境管理配置，支持语法高亮、历史版本差异对比、快速回滚，并提供Restful风格的API</li>
<li>域名管理：基于Tornado实现，支持多区域智能解析、可视化Bind操作、操作日志记录</li>
<li>运维工具：基于Tornado实现，运维场景中常用的加密解密、事件、故障、项目记录、提醒、报警等</li>
</ul>
<h3 id="在线体验"><a href="#在线体验" class="headerlink" title="在线体验"></a>在线体验</h3><p>CoDo提供了在线Demo供使用者体验，Demo账号只有部分权限</p>
<p> - 地址：<a target="_blank" rel="noopener" href="http://demo.opendevops.cn/">http://demo.opendevops.cn</a></p>
<p> - 用户：demo</p>
<p> - 密码：2ZbFYNv9WibWcR7GB6kcEY</p>
<p><img src="http://iubest.gitee.io/pic/640.jpg" alt="img"></p>
<h2 id="推荐理由"><a href="#推荐理由" class="headerlink" title="推荐理由"></a>推荐理由</h2><p><strong>团队出品：</strong> github上有很多开源的devops工具，几乎全部都由个人发布维护，代码质量、版本进度以及可持续性都无法保障，陷入不能用或不敢用的尴尬境地，CoDo非个人项目，由一个团队负责开发维护，有幸我也是团队中一员，参与贡献了部分代码，所以在稳定性和持续性方面更有保证</p>
<p><strong>生产实践：</strong> CoDo核心代码贡献者全部来自于一线运维团队，团队成员从运维需求出发，致力于解决运维痛点，更了解运维的需求，且核心代码经过了多年生产实践，并非实验产品，运行稳定</p>
<p><strong>功能齐全：</strong> CoDo采用微服务的理念构建，模块化开发，目前已有资产管理、定时任务、任务调度、配置中心、域名管理、运维工具几大模块，支持持续集成、持续部署、代码审查、数据库审核与优化建议等众多功能，覆盖大部分的运维场景，让你不再费心劳神在多个系统间奔波，一个平台全搞定</p>
<p><strong>完善支持：</strong> CoDo除了提供专业的文档支持外，还同时开始录制一些基础的部署使用视频帮助初学者快速上手，如果你觉得这些还不够，我们也提供QQ或微信远程支持，助你顺利部署使用</p>
<p><strong>开源免费：</strong> 这是一个开源项目，所有功能均可免费使用，源码托管在GitHub</p>
<h2 id="项目地址"><a href="#项目地址" class="headerlink" title="项目地址"></a>项目地址</h2><p>官网：<a target="_blank" rel="noopener" href="http://www.opendevops.cn/">http://www.opendevops.cn</a></p>
<p>GitHub：<a target="_blank" rel="noopener" href="https://github.com/opendevops-cn">https://github.com/opendevops-cn</a></p>
<p>文档地址：<a target="_blank" rel="noopener" href="http://docs.opendevops.cn/zh/latest">http://docs.opendevops.cn/zh/latest</a></p>
<p>安装视频：<a target="_blank" rel="noopener" href="https://www.bilibili.com/video/av53446517">https://www.bilibili.com/video/av53446517</a></p>
<p>最后欢迎大家使用，如有任何意见和建议都可以通过ISSUE或者QQ群反馈给我们，我们会进行持续的更新和优化</p>
 
      <!-- reward -->
      
    </div>
    

    <!-- copyright -->
    
    <footer class="article-footer">
       
  <ul class="article-tag-list" itemprop="keywords"><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/develop/" rel="tag">develop</a></li></ul>

    </footer>
  </div>

    
 
   
</article>

    
    <article
  id="post-develop/手把手教你Pycharm远程连接服务器端项目进行本地开发调试"
  class="article article-type-post"
  itemscope
  itemprop="blogPost"
  data-scroll-reveal
>
  <div class="article-inner">
    
    <header class="article-header">
       
<h2 itemprop="name">
  <a class="article-title" href="/2020/11/11/develop/%E6%89%8B%E6%8A%8A%E6%89%8B%E6%95%99%E4%BD%A0Pycharm%E8%BF%9C%E7%A8%8B%E8%BF%9E%E6%8E%A5%E6%9C%8D%E5%8A%A1%E5%99%A8%E7%AB%AF%E9%A1%B9%E7%9B%AE%E8%BF%9B%E8%A1%8C%E6%9C%AC%E5%9C%B0%E5%BC%80%E5%8F%91%E8%B0%83%E8%AF%95/"
    >手把手教你Pycharm远程连接服务器端项目进行本地开发调试.md</a> 
</h2>
 

    </header>
     
    <div class="article-meta">
      <a href="/2020/11/11/develop/%E6%89%8B%E6%8A%8A%E6%89%8B%E6%95%99%E4%BD%A0Pycharm%E8%BF%9C%E7%A8%8B%E8%BF%9E%E6%8E%A5%E6%9C%8D%E5%8A%A1%E5%99%A8%E7%AB%AF%E9%A1%B9%E7%9B%AE%E8%BF%9B%E8%A1%8C%E6%9C%AC%E5%9C%B0%E5%BC%80%E5%8F%91%E8%B0%83%E8%AF%95/" class="article-date">
  <time datetime="2020-11-10T16:00:00.000Z" itemprop="datePublished">2020-11-11</time>
</a> 
  <div class="article-category">
    <a class="article-category-link" href="/categories/develop/">develop</a>
  </div>
   
    </div>
      
    <div class="article-entry" itemprop="articleBody">
       
  <h1 id="手把手教你Pycharm远程连接服务器端项目进行本地开发调试"><a href="#手把手教你Pycharm远程连接服务器端项目进行本地开发调试" class="headerlink" title="手把手教你Pycharm远程连接服务器端项目进行本地开发调试"></a>手把手教你Pycharm远程连接服务器端项目进行本地开发调试</h1><h2 id="pycharm同步服务器项目"><a href="#pycharm同步服务器项目" class="headerlink" title="pycharm同步服务器项目"></a>pycharm同步服务器项目</h2><p>其实整体的步骤不是很复杂，但是也需要你仔细的执行每一步。</p>
<p><strong>首先</strong>，你需要从git或者通过上传的方式在服务器端和你的本地端下载好你的项目文件。</p>
<p>本地项目路径(我的是mac)：<br>/Users/kuls/Desktop/MyProject</p>
<p>服务器项目路径：<br>/root/MyProject</p>
<p><strong>第二步</strong>，打开pycharm的Tools-&gt;Deployment-&gt;Configuration</p>
<p><img src="http://iubest.gitee.io/pic/640-1601173208618.png" alt="img"></p>
<p><strong>第三步</strong>，创建一个sftp，并且填写服务器相关信息。</p>
<p><img src="http://iubest.gitee.io/pic/640-1601173208523.png" alt="img"></p>
<p>在Mappings中填写你本地项目的路径以及服务器上的路径。Web Path我们暂时不做考虑。</p>
<p><img src="http://iubest.gitee.io/pic/640-1601173208456.png" alt="img"></p>
<p><img src="http://iubest.gitee.io/pic/640-1601173242716.png" alt="img"></p>
<p>当然你也可以不同步项目中的某些文件，根据自己需求来。</p>
<p><img src="http://iubest.gitee.io/pic/640-1601173211899.png" alt="img"></p>
<p>点击OK！</p>
<p><strong>第四步</strong>，就是在本地连上服务器端的运行环境。</p>
<p><img src="http://iubest.gitee.io/pic/640-1601173208763.png" alt="img"></p>
<p>找到Project Interpreter</p>
<p><img src="http://iubest.gitee.io/pic/640-1601173208517.png" alt="img"></p>
<p>点击add</p>
<p><img src="http://iubest.gitee.io/pic/640-1601173208765.png" alt="img"></p>
<p>填写相关信息<img src="http://iubest.gitee.io/pic/640-1601173208330.png" alt="img"></p>
<p>填写环境路径</p>
<p><img src="http://iubest.gitee.io/pic/640-1601173208386.png" alt="img"></p>
<p>填写项目文件路径，包含本地和服务器</p>
<p><img src="http://iubest.gitee.io/pic/640-1601173208460.png" alt="img"></p>
<p><img src="http://iubest.gitee.io/pic/640-1601173208457.png" alt="img"></p>
<p>填写完毕后，我们可以发现，环境中的依赖都能够看到了</p>
<p><img src="http://iubest.gitee.io/pic/640-1601173208674.png" alt="img"></p>
<p>到目前为止，基本操作都已经完成了。我们还需要完善的是把自动同步的开关打开，当然你也可以通过保存文件的形式来同步(当你保存的时候会自动帮你同步)<img src="http://iubest.gitee.io/pic/640-1601173212100.png" alt="img"></p>
<p>除此之外，你也可以自己编写一些指令来本地运行调试你的远程项目！</p>
<p><img src="http://iubest.gitee.io/pic/640-1601173208523.png" alt="img"></p>
<h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>Pycharm确实是一个非常非常好的IDE工具，有很多很多的功能我们可能还没有发现，所以大家要不断的去探索和学习。虽然这些IDE帮助我们更便捷的开发，我们也不能完全依赖于它，也要懂得背后的原理。</p>
 
      <!-- reward -->
      
    </div>
    

    <!-- copyright -->
    
    <footer class="article-footer">
       
  <ul class="article-tag-list" itemprop="keywords"><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/develop/" rel="tag">develop</a></li></ul>

    </footer>
  </div>

    
 
   
</article>

    
    <article
  id="post-develop/掌握 Python 的 Lambda 函数"
  class="article article-type-post"
  itemscope
  itemprop="blogPost"
  data-scroll-reveal
>
  <div class="article-inner">
    
    <header class="article-header">
       
<h2 itemprop="name">
  <a class="article-title" href="/2020/11/11/develop/%E6%8E%8C%E6%8F%A1%20Python%20%E7%9A%84%20Lambda%20%E5%87%BD%E6%95%B0/"
    >掌握 Python 的 Lambda 函数.md</a> 
</h2>
 

    </header>
     
    <div class="article-meta">
      <a href="/2020/11/11/develop/%E6%8E%8C%E6%8F%A1%20Python%20%E7%9A%84%20Lambda%20%E5%87%BD%E6%95%B0/" class="article-date">
  <time datetime="2020-11-10T16:00:00.000Z" itemprop="datePublished">2020-11-11</time>
</a> 
  <div class="article-category">
    <a class="article-category-link" href="/categories/develop/">develop</a>
  </div>
   
    </div>
      
    <div class="article-entry" itemprop="articleBody">
       
  <h1 id="掌握-Python-的-Lambda-函数"><a href="#掌握-Python-的-Lambda-函数" class="headerlink" title="掌握 Python 的 Lambda 函数"></a>掌握 Python 的 Lambda 函数</h1><p>Lambda 函数是 Python 中的匿名函数。当你需要完成一件小工作时，在本地环境中使用它们可以让工作得心应手。有些人将它们简称为 lambdas，它们的语法如下：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">lambda arguments: expression  </span><br><span class="line"></span><br></pre></td></tr></table></figure>

<p><code>lambda</code> 关键字可以用来创建一个 lambda 函数，紧跟其后的是参数列表和用冒号分割开的单个表达式。例如，<code>lambda x: 2 * x</code> 是将任何输入的数乘2，而 <code>lambda x, y: x+y</code> 是计算两个数字的和。语法十分直截了当，对吧？</p>
<p>假设您知道什么是 lambda 函数，本文旨在提供有关如何正确使用 lambda 函数的一些常规准则。</p>
<h2 id="1-不要返回任何值"><a href="#1-不要返回任何值" class="headerlink" title="1. 不要返回任何值"></a>1. 不要返回任何值</h2><p>看看语法，您可能会注意到我们在 lambda 函数中并没有返回任何内容。这都是因为 lambda 函数只能包含一个表达式。然而，使用 <code>return</code> 关键字会构成不符合规定语法的语句，如下所示：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">&gt;&gt;&gt; integers &#x3D; [(3, -3), (2, 3), (5, 1), (-4, 4)]  </span><br><span class="line">&gt;&gt;&gt; sorted(integers, key&#x3D;lambda x: x[-1])  </span><br><span class="line">[(3, -3), (5, 1), (2, 3), (-4, 4)]  </span><br><span class="line">&gt;&gt;&gt; sorted(integers, key&#x3D;lambda x: return x[-1])  </span><br><span class="line">...   </span><br><span class="line">  File &quot;&lt;input&gt;&quot;, line 1  </span><br><span class="line">    sorted(integers, key&#x3D;lambda x: return x[-1])  </span><br><span class="line">                                   ^  </span><br><span class="line">SyntaxError: invalid syntax  </span><br><span class="line"></span><br></pre></td></tr></table></figure>

<p>该错误可能是由于无法区分表达式和语句而引起的。像是包含 <code>return</code>、<code>try</code>、 <code>with</code> 以及 <code>if</code> 的语句会执行特殊动作。然而，表达式指的是那些可以被计算出一个值的表达，例如数值或其他 Python 对象。</p>
<p>通过使用 lambda 函数，单个表达式会被计算为一个值并且参与后续的计算，例如由 <code>sorted</code> 函数排序。</p>
<h2 id="2-不要忘记更好的选择"><a href="#2-不要忘记更好的选择" class="headerlink" title="2. 不要忘记更好的选择"></a>2. 不要忘记更好的选择</h2><p>lambda 函数最常见的使用场景是将它作为一些内置工具函数中 <code>key</code> 的实参，比如上面展示的 <code>sorted()</code> 和 <code>max()</code>。根据情况，我们可以使用其他替代方法。思考下面的例子：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">&gt;&gt;&gt; integers &#x3D; [-4, 3, 7, -5, -2, 6]  </span><br><span class="line">&gt;&gt;&gt; sorted(integers, key&#x3D;lambda x: abs(x))  </span><br><span class="line">[-2, 3, -4, -5, 6, 7]  </span><br><span class="line">&gt;&gt;&gt; sorted(integers, key&#x3D;abs)  </span><br><span class="line">[-2, 3, -4, -5, 6, 7]  </span><br><span class="line">&gt;&gt;&gt; scores &#x3D; [(93, 100), (92, 99), (95, 94)]  </span><br><span class="line">&gt;&gt;&gt; max(scores, key&#x3D;lambda x: x[0] + x[1])  </span><br><span class="line">(93, 100)  </span><br><span class="line">&gt;&gt;&gt; max(scores, key&#x3D;sum)  </span><br><span class="line">(93, 100)  </span><br><span class="line"></span><br></pre></td></tr></table></figure>

<p>在数据科学领域，很多人使用 pandas 库来处理数据。如下所示，我们可以使用 lambda 函数通过 <code>map()</code> 函数从现有数据中创建新数据。除了使用 lambda 函数外，我们还可以直接使用算术函数，因为 pandas 是支持的：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">&gt;&gt;&gt; import pandas as pd  </span><br><span class="line">&gt;&gt;&gt; data &#x3D; pd.Series([1, 2, 3, 4])  </span><br><span class="line">&gt;&gt;&gt; data.map(lambda x: x + 5)  </span><br><span class="line">0    6  </span><br><span class="line">1    7  </span><br><span class="line">2    8  </span><br><span class="line">3    9  </span><br><span class="line">dtype: int64  </span><br><span class="line">&gt;&gt;&gt; data + 5  </span><br><span class="line">0    6  </span><br><span class="line">1    7  </span><br><span class="line">2    8  </span><br><span class="line">3    9  </span><br><span class="line">dtype: int64  </span><br><span class="line"></span><br></pre></td></tr></table></figure>

<h2 id="3-不要将它赋值给变量"><a href="#3-不要将它赋值给变量" class="headerlink" title="3. 不要将它赋值给变量"></a>3. 不要将它赋值给变量</h2><p>我曾见过一些人将 lambda 函数误认为是简单函数的另一种声明方式，您可能也见过有人像下面这么做：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">&gt;&gt;&gt; doubler &#x3D; lambda x: 2 * x  </span><br><span class="line">&gt;&gt;&gt; doubler(5)  </span><br><span class="line">10  </span><br><span class="line">&gt;&gt;&gt; doubler(7)  </span><br><span class="line">14  </span><br><span class="line">&gt;&gt;&gt; type(doubler)  </span><br><span class="line">&lt;class &#39;function&#39;&gt;  </span><br><span class="line"></span><br></pre></td></tr></table></figure>

<p>对 lambda 函数命名的唯一作用可能是出于教学目的，以表明 lambda 函数的确是和其他函数一样的函数——可以被调用并且具有某种功能。除此之外，我们不应该将 lambda 函数赋值给变量。</p>
<p>为 lambda 函数命名的问题在于这使得调试不那么直观。与其他的使用常规 <code>def</code> 关键字创建的函数不同，lambda 函数没有名字，这也是为什么有时它们被称为匿名函数的原因。思考下面简单的例子，找出细微的区别：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line">&gt;&gt;&gt; inversive0 &#x3D; lambda x: 1 &#x2F; x  </span><br><span class="line">&gt;&gt;&gt; inversive0(2)  </span><br><span class="line">0.5  </span><br><span class="line">&gt;&gt;&gt; inversive0(0)  </span><br><span class="line">Traceback (most recent call last):  </span><br><span class="line">  File &quot;&lt;input&gt;&quot;, line 1, in &lt;module&gt;  </span><br><span class="line">  File &quot;&lt;input&gt;&quot;, line 1, in &lt;lambda&gt;  </span><br><span class="line">ZeroDivisionError: division by zero  </span><br><span class="line">&gt;&gt;&gt; def inversive1(x):  </span><br><span class="line">...     return 1 &#x2F; x  </span><br><span class="line">...   </span><br><span class="line">&gt;&gt;&gt; inversive1(2)  </span><br><span class="line">0.5  </span><br><span class="line">&gt;&gt;&gt; inversive1(0)  </span><br><span class="line">Traceback (most recent call last):  </span><br><span class="line">  File &quot;&lt;input&gt;&quot;, line 1, in &lt;module&gt;  </span><br><span class="line">  File &quot;&lt;input&gt;&quot;, line 2, in inversive1  </span><br><span class="line">ZeroDivisionError: division by zero  </span><br><span class="line"></span><br></pre></td></tr></table></figure>

<ul>
<li><p>当您的代码存在关于 lambda 函数的问题（即 <code>inversive0</code>），<code>Traceback</code> 错误信息只会提示您 lambda 函数存在问题。</p>
</li>
<li><p>相比之下，使用正常定义的函数，<code>Traceback</code>会清晰地提示您有问题的函数（即 <code>inversive1</code>）。</p>
</li>
</ul>
<p>与此相关，如果您想多次使用 lambda 函数，最佳实践是使用通过 <code>def</code> 定义的允许使用文档字符串的常规函数。</p>
<h2 id="4-不要忘记列表推导式"><a href="#4-不要忘记列表推导式" class="headerlink" title="4. 不要忘记列表推导式"></a>4. 不要忘记列表推导式</h2><p>有些人喜欢将 lambda 函数和高阶函数一起使用，比如 <code>map</code> 或 <code>filter</code>。思考下面用法示例：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">&gt;&gt;&gt; # 创建一个数字列表  </span><br><span class="line">&gt;&gt;&gt; numbers &#x3D; [2, 1, 3, -3]  </span><br><span class="line">&gt;&gt;&gt; # 使用带有 lambda 函数的 map 函数  </span><br><span class="line">&gt;&gt;&gt; list(map(lambda x: x * x, numbers))  </span><br><span class="line">[4, 1, 9, 9]  </span><br><span class="line">&gt;&gt;&gt; # 使用带有 lambda 函数的 filter 函数  </span><br><span class="line">&gt;&gt;&gt; list(filter(lambda x: x % 2, numbers))  </span><br><span class="line">[1, 3, -3]  </span><br><span class="line"></span><br></pre></td></tr></table></figure>

<p>我们可以使用可读性更强的列表推导式代替 lambda 函数。如下所示，我们使用列表推导式来创建相同的列表对象。如您所见，与列表推导式相比，之前将 <code>map</code> 或 <code>filter</code> 函数与 lambda 函数一起使用更麻烦。因此，在创建涉及高阶函数的列表时，应考虑使用列表推导式。</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">&gt;&gt;&gt; # Use list comprehensions  </span><br><span class="line">&gt;&gt;&gt; [x * x for x in numbers]  </span><br><span class="line">[4, 1, 9, 9]  </span><br><span class="line">&gt;&gt;&gt; [x for x in numbers if x % 2]  </span><br><span class="line">[1, 3, -3]  </span><br><span class="line"></span><br></pre></td></tr></table></figure>

<h2 id="结论"><a href="#结论" class="headerlink" title="结论"></a>结论</h2><p>在本文中，我们回顾了使用 lambda 函数可能会犯的四个常见错误。通过避免这些错误，您应该能在代码中正确使用 lambda 函数。</p>
<p>使用 lambda 函数的经验准则是保持简单以及只在本地使用一次。</p>
 
      <!-- reward -->
      
    </div>
    

    <!-- copyright -->
    
    <footer class="article-footer">
       
  <ul class="article-tag-list" itemprop="keywords"><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/develop/" rel="tag">develop</a></li></ul>

    </footer>
  </div>

    
 
   
</article>

    
    <article
  id="post-develop/推荐这款开源、轻量无 Agent 自动化运维平台"
  class="article article-type-post"
  itemscope
  itemprop="blogPost"
  data-scroll-reveal
>
  <div class="article-inner">
    
    <header class="article-header">
       
<h2 itemprop="name">
  <a class="article-title" href="/2020/11/11/develop/%E6%8E%A8%E8%8D%90%E8%BF%99%E6%AC%BE%E5%BC%80%E6%BA%90%E3%80%81%E8%BD%BB%E9%87%8F%E6%97%A0%20Agent%20%E8%87%AA%E5%8A%A8%E5%8C%96%E8%BF%90%E7%BB%B4%E5%B9%B3%E5%8F%B0/"
    >推荐这款开源、轻量无 Agent 自动化运维平台.md</a> 
</h2>
 

    </header>
     
    <div class="article-meta">
      <a href="/2020/11/11/develop/%E6%8E%A8%E8%8D%90%E8%BF%99%E6%AC%BE%E5%BC%80%E6%BA%90%E3%80%81%E8%BD%BB%E9%87%8F%E6%97%A0%20Agent%20%E8%87%AA%E5%8A%A8%E5%8C%96%E8%BF%90%E7%BB%B4%E5%B9%B3%E5%8F%B0/" class="article-date">
  <time datetime="2020-11-10T16:00:00.000Z" itemprop="datePublished">2020-11-11</time>
</a> 
  <div class="article-category">
    <a class="article-category-link" href="/categories/develop/">develop</a>
  </div>
   
    </div>
      
    <div class="article-entry" itemprop="articleBody">
       
  <h1 id="推荐这款开源、轻量无-Agent-自动化运维平台"><a href="#推荐这款开源、轻量无-Agent-自动化运维平台" class="headerlink" title="推荐这款开源、轻量无 Agent 自动化运维平台"></a>推荐这款开源、轻量无 Agent 自动化运维平台</h1><p>在日常运维管理的发展过程中，可视化、自动化是一个阶段的进程必备要素，所以，对于可视化运维平台的掌握与了解也非常重要，我们运维小伙伴们也在不断的探索与挖掘当中，今天，民工哥给大家安利一款可视化的自动化运维管理平台：Spug，开源、免费，功能强大。</p>
<h2 id="Spug简介"><a href="#Spug简介" class="headerlink" title="Spug简介"></a>Spug简介</h2><p>Spug面向中小型企业设计的轻量级无 Agent 的自动化运维平台，整合了主机管理、主机批量执行、主机在线终端、文件在线上传下载、应用发布部署、在线任务计划、配置中心、监控、报警等一系列功能。</p>
<ul>
<li>代码仓库地址：<a target="_blank" rel="noopener" href="https://github.com/openspug/spug.dev">https://github.com/openspug/spug.dev</a></li>
<li>官网地址：<a target="_blank" rel="noopener" href="https://www.spug.dev/">https://www.spug.dev</a></li>
<li>使用文档：<a target="_blank" rel="noopener" href="https://www.spug.dev/docs/about-spug/">https://www.spug.dev/docs/about-spug/</a></li>
<li>更新日志：<a target="_blank" rel="noopener" href="https://www.spug.dev/docs/change-log/">https://www.spug.dev/docs/change-log/</a></li>
<li>常见问题：<a target="_blank" rel="noopener" href="https://www.spug.dev/docs/faq/">https://www.spug.dev/docs/faq/</a></li>
</ul>
<h2 id="Spug的功能"><a href="#Spug的功能" class="headerlink" title="Spug的功能"></a>Spug的功能</h2><ul>
<li>批量执行: 主机命令在线批量执行</li>
<li>在线终端: 主机支持浏览器在线终端登录</li>
<li>文件管理: 主机文件在线上传下载</li>
<li>任务计划: 灵活的在线任务计划</li>
<li>发布部署: 支持自定义发布部署流程</li>
<li>配置中心: 支持 KV、文本、json 等格式的配置</li>
<li>监控中心: 支持站点、端口、进程、自定义等监控</li>
<li>报警中心: 支持短信、邮件、钉钉、微信等报警方式</li>
<li>优雅美观: 基于 Ant Design 的 UI 界面</li>
<li>开源免费: 前后端代码完全开源</li>
</ul>
<h2 id="安装环境要求"><a href="#安装环境要求" class="headerlink" title="安装环境要求"></a>安装环境要求</h2><ul>
<li>Python 3.6+</li>
<li>Django 2.2</li>
<li>Node 12.14</li>
<li>React 16.11</li>
</ul>
<h2 id="安装Spug"><a href="#安装Spug" class="headerlink" title="安装Spug"></a>安装Spug</h2><p>简化一切安装操作步骤，官方也建议使用docker进行安装，那么，接下来就使用docker来安装这款工具平台。本文操作基于Centos7.x操作系统。</p>
<p><strong>1. 安装docker并启动</strong></p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">yum install docker -y</span><br><span class="line">systemctl start docker</span><br></pre></td></tr></table></figure>

<p><strong>2. 拉取镜像</strong></p>
<p>阿里云的镜像与 Docker hub 同步更新，国内用户建议使用阿里云的镜像。</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ docker pull registry.aliyuncs.com&#x2F;openspug&#x2F;spug</span><br></pre></td></tr></table></figure>

<p><strong>3. 启动容器</strong></p>
<p>Docker镜像内部使用的 Mysql 数据库。如果需要持久化存储代码和数据，可以添加：-v 映射容器内/data路径</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">$ docker run -d --name&#x3D;spug -p 80:80 registry.aliyuncs.com&#x2F;openspug&#x2F;spug</span><br><span class="line"></span><br><span class="line"># 持久化存储启动命令：</span><br><span class="line"># mydata是本地磁盘路径，&#x2F;data是容器内代码和数据初始化存储的路径</span><br><span class="line">$ docker run -d --name&#x3D;spug -p 80:80 -v &#x2F;mydata&#x2F;:&#x2F;data registry.aliyuncs.com&#x2F;openspug&#x2F;spug</span><br></pre></td></tr></table></figure>

<p><strong>4. 初始化</strong></p>
<p>以下操作会创建一个用户名为 admin 密码为 spug.dev 的管理员账户，可自行替换管理员账户。</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">$ docker exec spug init_spug admin spug.dev</span><br><span class="line"></span><br><span class="line"># 执行完毕后需要重启容器</span><br><span class="line">$ docker restart spug</span><br></pre></td></tr></table></figure>

<p><strong>5. 访问测试</strong></p>
<p>在浏览器中输入 <a target="_blank" rel="noopener" href="http://localhost/">http://localhost:80</a> 访问,用户名：admin  密码：spug.dev</p>
<p><img src="http://iubest.gitee.io/pic/640-1601003929840.webp" alt="img"></p>
<h2 id="使用介绍"><a href="#使用介绍" class="headerlink" title="使用介绍"></a>使用介绍</h2><p>登录完成后，就可以看到主界面，如下</p>
<p><img src="http://iubest.gitee.io/pic/640-1601003929908.webp" alt="img"></p>
<p><strong>主机管理</strong></p>
<p>管理维护平台可操作的主机，首次添加主机时需要输入 ssh 指定用户的密码。</p>
<p><img src="http://iubest.gitee.io/pic/640-1601003929906.webp" alt="img"></p>
<p><strong>批量执行</strong></p>
<p>包含维护命令模版和批量远程执行命令两部分功能，常用来执行一些临时的任务例如，批量安装/卸载某个依赖包等。</p>
<p><img src="http://iubest.gitee.io/pic/640-1601003929745.webp" alt="img"></p>
<ul>
<li>执行任务</li>
</ul>
<p>可以选择一到多个在主机管理中添加的主机作为执行的目标主机，命令内容可以直接写也支持从模板中读取已保存的命令。</p>
<ul>
<li>模板管理</li>
</ul>
<p>用于存储复杂、常用的命令集合，以便后期可随时使用。</p>
<p><strong>应用发布</strong></p>
<ul>
<li>应用管理</li>
</ul>
<p>管理和维护可发布的应用。每个应用又可以创建对应环境的发布配置，发布配置请查看发布配置文档。</p>
<p><img src="http://iubest.gitee.io/pic/640-1601003929710.webp" alt="img"></p>
<ul>
<li>发布配置</li>
</ul>
<p>配置指定应用在某环境下如何执行发布，发布支持两种方式 常规发布 和 自定义发布。</p>
<ul>
<li>发布申请</li>
</ul>
<p>创建和执行发布。</p>
<p><img src="http://iubest.gitee.io/pic/640-1601366764736.webp" alt="img"></p>
<p><strong>配置中心</strong></p>
<ul>
<li>环境管理</li>
</ul>
<p>管理应用的运行环境，一般包含开发环境、测试环境和生产环境，应用发布和配置管理需要用它来区分不同的环境。</p>
<p><img src="http://iubest.gitee.io/pic/640-1601003929907.png" alt="img"></p>
<ul>
<li>服务管理</li>
</ul>
<p>管理和维护应用依赖的服务配置。例如有两个应用 A 和应用 B，它们共同使用一个数据库，那么就可以把这个数据库提取出来作为单独的服务来管理。这样带来的好处是如果这个数据库配置变更了，那么只需要在服务管理里把这个数据库的配置更新即可，不必在多个应用之间切换查找更新。</p>
<p><img src="http://iubest.gitee.io/pic/640-1601003929824.webp" alt="img"></p>
<ul>
<li>应用管理</li>
</ul>
<p>用于维护应用的配置，应用配置包含 公共 和 私有 两种类型的配置。</p>
<ul>
<li>配置管理</li>
</ul>
<p>用户维护服务和应用在不同环境下的具体配置。</p>
<p><strong>任务调度</strong></p>
<p>维护一些周期性的任务</p>
<p><img src="http://iubest.gitee.io/pic/640-1601003929829.webp" alt="img"></p>
<p><strong>监控中心</strong></p>
<p>该模块提供了以下几种常用的监控模式</p>
<ul>
<li>站点检测</li>
</ul>
<p>通过 GET 请求指定的 url 匹配返回的状态码来确定站点是否异常</p>
<ul>
<li>端口检测</li>
</ul>
<p>检测指定目标主机的端口是否可以正常建立接连</p>
<ul>
<li>进程检测</li>
</ul>
<p>检测指定目标主机的某个进程是否存活</p>
<ul>
<li>自定义脚本检测</li>
</ul>
<p>在指定主机上运行自定义的脚本，通过判断返回的退出状态码来确定是否有异常</p>
<p><img src="http://iubest.gitee.io/pic/640-1601003929902.png" alt="img"></p>
<p><strong>报警中心</strong></p>
<p>配置与维护日常报警相关，如:报警记录、报警联系人与组</p>
<p><img src="http://iubest.gitee.io/pic/640-1601003929909.webp" alt="img"></p>
<p><strong>系统管理</strong></p>
<p>除了页面上对普通用的管理，Spug 还提供了 manage.py user 命令可用于管理员账户的管理操作。</p>
<ul>
<li>创建账户</li>
</ul>
<p>创建账户使用 manage.py user add 命令，用法示例如下</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">$ cd spug&#x2F;spug_api</span><br><span class="line">$ source venv&#x2F;bin&#x2F;activate</span><br><span class="line">$ python manage.py user add -u admin -p 123 -n 民工哥 -s</span><br></pre></td></tr></table></figure>

<p>Docker 安装的可以执行如下命令</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">$ docker exec spug python3 &#x2F;data&#x2F;spug&#x2F;spug_api&#x2F;manage.py user add -u admin -p 123 -n 民工哥 -s</span><br><span class="line">#上面的命令会创建个登录名为 admin 密码为 123 昵称为 民工哥 的管理员账户，注意最后的 -s 参数，如果携带了这个参数意味着该账户为管理员账户， 管理员账户可以不受任何限制的访问所有功能模块。</span><br></pre></td></tr></table></figure>

<ul>
<li>重置密码</li>
</ul>
<p>使用 manage.py user reset 命令来重置账户密码，用法示例如下</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">$ cd spug&#x2F;spug_api</span><br><span class="line">$ source venv&#x2F;bin&#x2F;activate</span><br><span class="line">$ python manage.py user reset -u admin -p abc</span><br></pre></td></tr></table></figure>

<p>Docker 安装的可以执行如下命令</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">$ docker exec spug python3 &#x2F;data&#x2F;spug&#x2F;spug_api&#x2F;manage.py user reset -u admin -p abc</span><br><span class="line">#上述操作会重置登录名为 admin 的账户的密码为 abc。</span><br></pre></td></tr></table></figure>

<ul>
<li>启用账户</li>
</ul>
<p>当页面上登录连续错误数次超过3次后账户自动转为禁用状态，普通用户可以通过 系统管理 / 账户管理 在页面是启用账户即可，但管理员账户需要使用如下命令来启用</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">$ cd spug&#x2F;spug_api</span><br><span class="line">$ source venv&#x2F;bin&#x2F;activate</span><br><span class="line">$ python manage.py user enable -u admin</span><br></pre></td></tr></table></figure>

<p>Docker 安装的可以执行如下命令</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ docker exec spug python3 &#x2F;data&#x2F;spug&#x2F;spug_api&#x2F;manage.py user enable -u admin</span><br></pre></td></tr></table></figure> 
      <!-- reward -->
      
    </div>
    

    <!-- copyright -->
    
    <footer class="article-footer">
       
  <ul class="article-tag-list" itemprop="keywords"><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/develop/" rel="tag">develop</a></li></ul>

    </footer>
  </div>

    
 
   
</article>

    
    <article
  id="post-develop/用Python操作Kubernetes的Job"
  class="article article-type-post"
  itemscope
  itemprop="blogPost"
  data-scroll-reveal
>
  <div class="article-inner">
    
    <header class="article-header">
       
<h2 itemprop="name">
  <a class="article-title" href="/2020/11/11/develop/%E7%94%A8Python%E6%93%8D%E4%BD%9CKubernetes%E7%9A%84Job/"
    >用Python操作Kubernetes的Job.md</a> 
</h2>
 

    </header>
     
    <div class="article-meta">
      <a href="/2020/11/11/develop/%E7%94%A8Python%E6%93%8D%E4%BD%9CKubernetes%E7%9A%84Job/" class="article-date">
  <time datetime="2020-11-10T16:00:00.000Z" itemprop="datePublished">2020-11-11</time>
</a> 
  <div class="article-category">
    <a class="article-category-link" href="/categories/develop/">develop</a>
  </div>
   
    </div>
      
    <div class="article-entry" itemprop="articleBody">
       
  <h1 id="用Python操作Kubernetes的Job"><a href="#用Python操作Kubernetes的Job" class="headerlink" title="用Python操作Kubernetes的Job"></a>用Python操作Kubernetes的Job</h1><p>关于Kubernetes的Python SDK，几乎只有官方项目的examples。关于Job的基本增删改查操作，可以参考job_crud.py。但是，这只是基本用法，缺乏一些实用细节。  </p>
<p>本文给出Python SDK操作Kubernetes Job的更多示例代码，以及相关解释。</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">pip install kubernetes  </span><br><span class="line"></span><br></pre></td></tr></table></figure>

<h3 id="初始化"><a href="#初始化" class="headerlink" title="初始化"></a>初始化</h3><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">from kubernetes.client import BatchV1Api  </span><br><span class="line">from kubernetes.config import load_kube_config  </span><br><span class="line">  </span><br><span class="line">load_kube_config()  </span><br><span class="line">batch &#x3D; BatchV1Api()  </span><br><span class="line"></span><br></pre></td></tr></table></figure>

<p><code>load_kube_config</code>是从默认位置，也就是<code>~/.kube/config</code>加载配置。如果在其它位置，可以通过第一个参数传入其路径。</p>
<p><code>BatchV1Api()</code>可以当做Job的客户端来用。命名上，Batch和Job是类似的概念，前者强调批量。</p>
<h3 id="创建Job"><a href="#创建Job" class="headerlink" title="创建Job"></a>创建Job</h3><p>以下来自官方样例job_crud.py。</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><span class="line">def create_job_object():  </span><br><span class="line">      </span><br><span class="line">    container &#x3D; client.V1Container(  </span><br><span class="line">        ,  </span><br><span class="line">        image&#x3D;&quot;perl&quot;,  </span><br><span class="line">        command&#x3D;[&quot;perl&quot;, &quot;-Mbignum&#x3D;bpi&quot;, &quot;-wle&quot;, &quot;print bpi(2000)&quot;])  </span><br><span class="line">      </span><br><span class="line">    template &#x3D; client.V1PodTemplateSpec(  </span><br><span class="line">        metadata&#x3D;client.V1ObjectMeta(labels&#x3D;&#123;&quot;app&quot;: &quot;pi&quot;&#125;),  </span><br><span class="line">        spec&#x3D;client.V1PodSpec(restart_policy&#x3D;&quot;Never&quot;, containers&#x3D;[container]))  </span><br><span class="line">      </span><br><span class="line">    spec &#x3D; client.V1JobSpec(  </span><br><span class="line">        template&#x3D;template,  </span><br><span class="line">        backoff_limit&#x3D;4)  </span><br><span class="line">      </span><br><span class="line">    job &#x3D; client.V1Job(  </span><br><span class="line">        api_version&#x3D;&quot;batch&#x2F;v1&quot;,  </span><br><span class="line">        kind&#x3D;&quot;Job&quot;,  </span><br><span class="line">        metadata&#x3D;client.V1ObjectMeta(name&#x3D;JOB_NAME),  </span><br><span class="line">        spec&#x3D;spec)  </span><br><span class="line">  </span><br><span class="line">    return job  </span><br><span class="line">  </span><br><span class="line">  </span><br><span class="line">def create_job(api_instance, job):  </span><br><span class="line">    api_response &#x3D; api_instance.create_namespaced_job(  </span><br><span class="line">        body&#x3D;job,  </span><br><span class="line">        namespace&#x3D;&quot;default&quot;)  </span><br><span class="line">    print(&quot;Job created. status&#x3D;&#39;%s&#39;&quot; % str(api_response.status))  </span><br><span class="line"></span><br></pre></td></tr></table></figure>

<p>虽然，根据官方教程这样的写法，也能得到可用的<code>V1Job</code>，拿去执行创建操作。但还是过于陌生和偏门，不如主流、常见的YAML方便、易读写。</p>
<p>这里该出两种更方便的做法。</p>
<h3 id="直接使用YAML"><a href="#直接使用YAML" class="headerlink" title="直接使用YAML"></a>直接使用YAML</h3><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">---  </span><br><span class="line">apiVersion: batch&#x2F;v1  </span><br><span class="line">kind: Job  </span><br><span class="line">metadata:  </span><br><span class="line">  name: hello  </span><br><span class="line">spec:  </span><br><span class="line">  template:  </span><br><span class="line">    spec:  </span><br><span class="line">      containers:  </span><br><span class="line">        - name: echo  </span><br><span class="line">          image: alpine:3.11  </span><br><span class="line">          args:  </span><br><span class="line">            - &#39;echo&#39;  </span><br><span class="line">            - &#39;Hello world!&#39;  </span><br><span class="line"></span><br></pre></td></tr></table></figure>

<p>以上是一个最精简的Job配置样例，</p>
<p>通过读取文件为dict，可以直接拿去使用。</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">from kubernetes.client import V1Job  </span><br><span class="line">import yaml  </span><br><span class="line">  </span><br><span class="line">with open(&#39;job.yaml&#39;) as file:  </span><br><span class="line">    cfg &#x3D; yaml.safe_load(file)  </span><br><span class="line">job &#x3D; batch.create_namespaced_job(namespace&#x3D;&#39;default&#39;, body&#x3D;cfg)  </span><br><span class="line">assert isinstance(job, V1Job)  </span><br><span class="line"></span><br></pre></td></tr></table></figure>

<p><code>create_namespaced_job</code>同样接受字典作为body输入，因此YAML配置可以读出后直接传入。</p>
<p>这里返回的<code>V1Job</code>只是创建时的状态，但是会包含更多集群中的信息。</p>
<h3 id="使用dict"><a href="#使用dict" class="headerlink" title="使用dict"></a>使用dict</h3><p>由于<code>create_namespaced_job</code>接受字典作为<code>body</code>输入，因此直接使用<code>dict</code>也是可行的。</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line">cfg &#x3D; &#123;  </span><br><span class="line">    &#39;apiVersion&#39;: &#39;batch&#x2F;v1&#39;,  </span><br><span class="line">    &#39;kind&#39;: &#39;Job&#39;,  </span><br><span class="line">    &#39;metadata&#39;: &#123;  </span><br><span class="line">        &#39;name&#39;: &#39;hello&#39;  </span><br><span class="line">    &#125;,  </span><br><span class="line">    &#39;spec&#39;: &#123;  </span><br><span class="line">        &#39;template&#39;: &#123;  </span><br><span class="line">            &#39;spec&#39;: &#123;  </span><br><span class="line">                &#39;restartPolicy&#39;:  </span><br><span class="line">                &#39;Never&#39;,  </span><br><span class="line">                &#39;containers&#39;: [&#123;  </span><br><span class="line">                    &#39;name&#39;: &#39;upload&#39;,  </span><br><span class="line">                    &#39;image&#39;: &#39;alpine:3.11&#39;,  </span><br><span class="line">                    &#39;args&#39;: [&#39;echo&#39;, &#39;Hello world!&#39;]  </span><br><span class="line">                &#125;]  </span><br><span class="line">            &#125;  </span><br><span class="line">        &#125;  </span><br><span class="line">    &#125;  </span><br><span class="line">&#125;  </span><br><span class="line">batch.create_namespaced_job(namespace&#x3D;&#39;default&#39;, body&#x3D;cfg)  </span><br><span class="line"></span><br></pre></td></tr></table></figure>

<p>由于<code>dict</code>结构与YAML相同，而又没有类的束缚，所以也很灵活方便。</p>
<p>此外，从YAML读出为<code>dict</code>后，也可以通过修改部分字段，达到动态变化的效果。这种结合YAML和dict的使用方式，是对官方用法的最佳替代。</p>
<h3 id="监控Job运行"><a href="#监控Job运行" class="headerlink" title="监控Job运行"></a>监控Job运行</h3><p>在创建Job后，通常需要监控Job的运行，做一些外围处理。轮询当然是下下策，而Kubernetes提供了一个<code>Watch</code>机制，通过接收Event，实现对状态变化的掌控。Event只有在状态变化时才会有，所以是非常理想的回调。</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">from kubernetes.client import V1Job  </span><br><span class="line">from kubernetes.watch import Watch  </span><br><span class="line">  </span><br><span class="line">job_name &#x3D; &#39;hello&#39;  </span><br><span class="line">watcher &#x3D; Watch()  </span><br><span class="line">for event in watcher.stream(  </span><br><span class="line">        batch.list_namespaced_job,  </span><br><span class="line">        namespace&#x3D;&#39;default&#39;,  </span><br><span class="line">        label_selector&#x3D;f&#39;job-name&#x3D;&#123;job_name&#125;&#39;,  </span><br><span class="line">):    </span><br><span class="line">    assert isinstance(event, dict)  </span><br><span class="line">    job &#x3D; event[&#39;object&#39;]  </span><br><span class="line">    assert isinstance(job, V1Job)  </span><br><span class="line"></span><br></pre></td></tr></table></figure>

<p><code>Watch().stream</code>就是前面说的理想回调，它第一个参数是列出类的函数，这里选择<code>list_namespaced_job</code>。后面的参数，都是<code>list_namespaced_job</code>的参数。除了必备的namespace以外，<code>label_selector也</code>是一个常用参数，可以避免关注无关的Job。每个Job在创建后，都会自动带一个<code>f&#39;job-name=&#123;job_name&#125;&#39;</code>的Label，可以借此筛选。<code>job_name</code>就是<code>metadata</code>里设置的<code>name</code>，如这里<code>job-name=hello</code>。</p>
<p>event是一个dict，只有三个值。其中<code>event[&#39;raw_object&#39;]</code>只是<code>event[&#39;object&#39;]</code>的<code>dict</code>形式，没有太大意义。<code>event[&#39;type&#39;]</code>常见三个值，对应增删改。</p>
<ul>
<li><p><code>ADDED</code>，创建时的信息，和<code>create_namespaced_job</code>的返回值通常没有区别。</p>
</li>
<li><p><code>MODIFIED</code>，Job状态变化时的信息。</p>
</li>
<li><p><code>DELETED</code>，Job删除时的信息。</p>
</li>
</ul>
<p>以上三个状态值，对其它类型的资源也是通用的，比如Pod、Deployment等。</p>
<h3 id="V1Job的使用"><a href="#V1Job的使用" class="headerlink" title="V1Job的使用"></a>V1Job的使用</h3><p>对于具体的<code>V1Job</code>实例，其它字段都是和创建时的配置差不多的，只是多一些集群中的具体信息。所以，常用的还是.status字段。</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line">&gt;&gt;&gt; from kubernetes.client import V1JobStatus  </span><br><span class="line">&gt;&gt;&gt; isinstance(job.status, V1JobStatus)  </span><br><span class="line">True  </span><br><span class="line">&gt;&gt;&gt; print(job.status)  </span><br><span class="line">&#123;&#39;active&#39;: None,  </span><br><span class="line"> &#39;completion_time&#39;: datetime.datetime(2020, 8, 10, 9, 49, 38, tzinfo&#x3D;tzutc()),  </span><br><span class="line"> &#39;conditions&#39;: [&#123;&#39;last_probe_time&#39;: datetime.datetime(2020, 8, 10, 9, 49, 38, tzinfo&#x3D;tzutc()),  </span><br><span class="line">   &#39;last_transition_time&#39;: datetime.datetime(2020, 8, 10, 9, 49, 38, tzinfo&#x3D;tzutc()),  </span><br><span class="line">   &#39;message&#39;: None,  </span><br><span class="line">   &#39;reason&#39;: None,  </span><br><span class="line">   &#39;status&#39;: &#39;True&#39;,  </span><br><span class="line">   &#39;type&#39;: &#39;Complete&#39;&#125;],  </span><br><span class="line"> &#39;failed&#39;: None,  </span><br><span class="line"> &#39;start_time&#39;: datetime.datetime(2020, 8, 10, 9, 49, 32, tzinfo&#x3D;tzutc()),  </span><br><span class="line"> &#39;succeeded&#39;: 1&#125;  </span><br><span class="line"></span><br></pre></td></tr></table></figure>

<p>直接使用<code>job.status.succeeded</code>，可以得到成功的Container数量。下面几乎每一级都有特定的类，可以连续使用.操作符。如果有特殊需要，也可以用<code>job.to_dict()</code>转换成字典来用。</p>
<p>其它字段，作用基本上也和名称相关，不难推测。</p>
<h3 id="列出Job"><a href="#列出Job" class="headerlink" title="列出Job"></a>列出Job</h3><p>列出所有Job的<code>list_job_for_all_namespaces</code>不常用，一般只列出指定Namespace的Job。</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">from kubernetes.client import V1JobList, V1Job  </span><br><span class="line">  </span><br><span class="line">job_list &#x3D; batch.list_namespaced_job(namespace&#x3D;&#39;default&#39;)  </span><br><span class="line">assert isinstance(job_list, V1JobList)  </span><br><span class="line">  </span><br><span class="line">assert isinstance(job_list.items, list)  </span><br><span class="line">for job in job_list.items:  </span><br><span class="line">    assert isinstance(job, V1Job)  </span><br><span class="line"></span><br></pre></td></tr></table></figure>

<p>与监控的示例相比，这里去掉了<code>label_selector</code>，可以获取Namespace中所有的Job。如果有需要，可以通过自定义Label把所有Job分类，并使用l<code>abel_selector</code>获取指定类型的Job。</p>
<h3 id="读取Job"><a href="#读取Job" class="headerlink" title="读取Job"></a>读取Job</h3><p>如果知道Job的<code>name</code>，可以直接通过<code>read_*</code>系列接口，获得指定的<code>V1Job</code>。</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">from kubernetes.client import V1Job  </span><br><span class="line">  </span><br><span class="line">job &#x3D; batch.read_namespaced_job(name&#x3D;&#39;hello&#39;, namespace&#x3D;&#39;default&#39;)  </span><br><span class="line">assert isinstance(job, V1Job)  </span><br><span class="line"></span><br></pre></td></tr></table></figure>

<p>如果更看重状态，可以改用<code>read_namespaced_job_status</code>。虽然访问的API不同，但在Python的<code>V1Job</code>这个结果层面，没有本质差异。</p>
<h3 id="列出一个Job的Pod"><a href="#列出一个Job的Pod" class="headerlink" title="列出一个Job的Pod"></a>列出一个Job的Pod</h3><p>Pod是Kubernetes调度的最小单元，也是最常用的一种资源。</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">from typing import List  </span><br><span class="line">  </span><br><span class="line">from kubernetes.client import CoreV1Api, V1Pod  </span><br><span class="line">  </span><br><span class="line">  </span><br><span class="line">def get_pods_by(job_name: str) -&gt; List[V1Pod]:  </span><br><span class="line">    core &#x3D; CoreV1Api()  </span><br><span class="line">    pods &#x3D; core.list_namespaced_pod(  </span><br><span class="line">        namespace&#x3D;&#39;default&#39;,  </span><br><span class="line">        label_selector&#x3D;f&#39;job-name&#x3D;&#123;job_name&#125;&#39;,  </span><br><span class="line">        limit&#x3D;1,  </span><br><span class="line">    )  </span><br><span class="line">    return pods.items  </span><br><span class="line"></span><br></pre></td></tr></table></figure>

<p>这里的<code>get_pods_by</code>，可以用<code>job_name</code>获取对应的Pod。<code>limit=1</code>是在已知Pod只有一个的情况下做出的优化，可按需调整或去掉。</p>
<h3 id="删除Job"><a href="#删除Job" class="headerlink" title="删除Job"></a>删除Job</h3><p>删除一个Job：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">from kubernetes.client import V1Status  </span><br><span class="line">  </span><br><span class="line">status &#x3D; batch.delete_namespaced_job(  </span><br><span class="line">    namespace&#x3D;&#39;default&#39;,  </span><br><span class="line">    name&#x3D;job_name,  </span><br><span class="line">    propagation_policy&#x3D;&#39;Background&#39;,  </span><br><span class="line">)  </span><br><span class="line">assert isinstance(status, V1Status)  </span><br><span class="line"></span><br></pre></td></tr></table></figure>

<p>其中，<code>propagation_policy=&#39;Background&#39;</code>是不可省略的关键，否则默认是<code>Orphan</code>，其Pod不会被删除。这属于API设计的一个失误，与<code>kubectl</code>的默认行为不符合。而且，应该没有人在删除了Job之后，还要保留Pod的吧。这里也可以选择<code>&#39;Foreground&#39;</code>，阻塞等待相关资源的删除完毕。</p>
<p>删除多个、或所有Job：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">status &#x3D; batch.delete_collection_namespaced_job(  </span><br><span class="line">    namespace&#x3D;&#39;default&#39;,  </span><br><span class="line">    propagation_policy&#x3D;&#39;Background&#39;,  </span><br><span class="line">    label_selector&#x3D;&#39;some-label&#x3D;your-value&#39;,  </span><br><span class="line">)  </span><br><span class="line">assert isinstance(status, V1Status)  </span><br><span class="line"></span><br></pre></td></tr></table></figure>

<p>如果没有<code>label_selector</code>，那就是删除一个<code>Namespace</code>中的所有Job。</p>
<h3 id="更新Job"><a href="#更新Job" class="headerlink" title="更新Job"></a>更新Job</h3><p>这个比较少用，因为一般都是建新的。用法其实和<code>create_namespaced_job</code>差不多，参考官方样例即可。</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">def update_job(api_instance, job):  </span><br><span class="line">      </span><br><span class="line">    job.spec.template.spec.containers[0].image &#x3D; &quot;perl&quot;  </span><br><span class="line">    api_response &#x3D; api_instance.patch_namespaced_job(  </span><br><span class="line">        name&#x3D;JOB_NAME,  </span><br><span class="line">        namespace&#x3D;&quot;default&quot;,  </span><br><span class="line">        body&#x3D;job)  </span><br><span class="line">    print(&quot;Job updated. status&#x3D;&#39;%s&#39;&quot; % str(api_response.status))  </span><br><span class="line"></span><br></pre></td></tr></table></figure>

<h3 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h3><p>用Python操作Kubernetes的Job，总体上还是比较方便的。</p>
<p>作者：匿蟒</p>
<p>原文链接：<a target="_blank" rel="noopener" href="https://note.qidong.name/2020/08/python-k8s-job/">https://note.qidong.name/2020/08/python-k8s-job/</a></p>
<p>END</p>
<hr>
<p>                                </p>
<p>            **点击如下图片，了解<br>**</p>
<p>      运维+测试+开发+架构全栈进阶之路  </p>
<hr>
<h2 id="技术交流"><a href="#技术交流" class="headerlink" title="技术交流"></a>技术交流</h2><p>学无止境，了解更多关于kubernetes/docker/devops/openstack/openshift/linux/IaaS/PaaS相关内容，想要获取更多资料和免费视频，可按如下方式进入技术交流群</p>
<p><img src=""></p>
<p><strong>微信：</strong>luckylucky421302  </p>
<p>微信公众号  </p>
<hr>
<p> <strong>长按指纹关注公众号</strong>👇**<br>**</p>
<p><img src="">              </p>
<p>往期精彩文章  </p>
<p><a target="_blank" rel="noopener" href="http://mp.weixin.qq.com/s?__biz=MzU0NjEwMTg4Mg==&mid=2247484708&idx=1&sn=2a7230ade0ce0bd9059e92752e03ba18&chksm=fb638a31cc14032772d778f1a8fa5dc73f9aba6c99546c9ab25d9f91f706be3cff34572f0c1a&scene=21#wechat_redirect">kubernetes全栈技术+企业案例演示【带你快速掌握和使用k8s】</a></p>
<p><a target="_blank" rel="noopener" href="http://mp.weixin.qq.com/s?__biz=MzU0NjEwMTg4Mg==&mid=2247486272&idx=1&sn=e993bc259db899b79922863859d24bb2&chksm=fb638455cc140d43dd091b09efc0c744b82f32d905c78ce9be71aae8c6d324d625f110fa1093&scene=21#wechat_redirect">突破运维和开发瓶颈、Python、k8s、DevOps转型一网打尽！</a></p>
<p><a target="_blank" rel="noopener" href="http://mp.weixin.qq.com/s?__biz=MzU0NjEwMTg4Mg==&mid=2247487181&idx=1&sn=8cb290c13cc5d7bec65abac1a668e6be&chksm=fb6381d8cc1408cee263ae5c2d8d63c899001eb45a56197f5480ef66e9998d1e9cd15e203e0c&scene=21#wechat_redirect">运维+测试+开发+架构进阶之路</a>  </p>
<p><a target="_blank" rel="noopener" href="http://mp.weixin.qq.com/s?__biz=MzU0NjEwMTg4Mg==&mid=2247486031&idx=1&sn=58003addbf36f2858f7d0dfbf887bb2a&chksm=fb63855acc140c4cf0346cc3433bc874e7cd37a5b17cf5254147a1b20b538cf70158f2234b0f&scene=21#wechat_redirect">python运维开发实战-基础篇  
</a></p>
<p><a target="_blank" rel="noopener" href="http://mp.weixin.qq.com/s?__biz=MzU0NjEwMTg4Mg==&mid=2247486201&idx=1&sn=0d45a2660a7480d73cf72ba67fe52f3a&chksm=fb6385eccc140cfa90bef6759c768c94ac2ca904c827fb2dcd969858ba46b818145e76e09c62&scene=21#wechat_redirect">python运维和开发实战-高级篇</a>  </p>
<p><a target="_blank" rel="noopener" href="http://mp.weixin.qq.com/s?__biz=MzU0NjEwMTg4Mg==&mid=2247486210&idx=1&sn=078e172a34c22e5db1f57b17c21b2efe&chksm=fb638417cc140d011a058d7cf83795bf75cfc327acbb3727c968058ff2807874a26f87843080&scene=21#wechat_redirect">python运维和开发实战-安装和创建Django项目</a></p>
<p><a target="_blank" rel="noopener" href="http://mp.weixin.qq.com/s?__biz=MzU0NjEwMTg4Mg==&mid=2247484220&idx=1&sn=72ae437836108fbecbeb86752aad888c&chksm=fb638c29cc14053fafcb62be244f5a279052b8d159c9c530e73495cfd8ab97205988780d4aaa&scene=21#wechat_redirect">谈谈我的IT发展之路</a></p>
<p><a target="_blank" rel="noopener" href="http://mp.weixin.qq.com/s?__biz=MzU0NjEwMTg4Mg==&mid=2247484445&idx=1&sn=bf8928e7aae815b8e7f9ac56a67289ce&chksm=fb638b08cc14021e5e025f2f0b89d18ec5ef19f9dec4429a56f356e8b596d01095e313dab3e3&scene=21#wechat_redirect">Prometheus+Grafana+Alertmanager搭建全方位的监控告警系统-超详细文档</a></p>
<p><a target="_blank" rel="noopener" href="http://mp.weixin.qq.com/s?__biz=MzU0NjEwMTg4Mg==&mid=2247484264&idx=1&sn=4d74de0cb117c07ba7c73ea21c42bf94&chksm=fb638c7dcc14056b9f0261e05e4d36a057c4511fb5806222f71b7965c3573fed86672b2c93b7&scene=21#wechat_redirect">k8s1.18多master节点高可用集群安装-超详细中文官方文档</a></p>
<p><a target="_blank" rel="noopener" href="http://mp.weixin.qq.com/s?__biz=MzIzOTQ3NDg1Mw==&mid=2247483668&idx=1&sn=a7d0768b7eb034ef2e2f23e25b1b1845&chksm=e928c0dede5f49c878898e04329f08d8adf554397476283aaa3f2554f71a8bf20986755fbcc9&scene=21#wechat_redirect">linux面试题汇总</a></p>
<p><a target="_blank" rel="noopener" href="http://mp.weixin.qq.com/s?__biz=MzU0NjEwMTg4Mg==&mid=2247486210&idx=1&sn=078e172a34c22e5db1f57b17c21b2efe&chksm=fb638417cc140d011a058d7cf83795bf75cfc327acbb3727c968058ff2807874a26f87843080&scene=21#wechat_redirect">python运维和开发实战-安装和创建Django项目</a></p>
 
      <!-- reward -->
      
    </div>
    

    <!-- copyright -->
    
    <footer class="article-footer">
       
  <ul class="article-tag-list" itemprop="keywords"><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/develop/" rel="tag">develop</a></li></ul>

    </footer>
  </div>

    
 
   
</article>

    
    <article
  id="post-develop/相见恨晚！Python那些不为人知的大坑"
  class="article article-type-post"
  itemscope
  itemprop="blogPost"
  data-scroll-reveal
>
  <div class="article-inner">
    
    <header class="article-header">
       
<h2 itemprop="name">
  <a class="article-title" href="/2020/11/11/develop/%E7%9B%B8%E8%A7%81%E6%81%A8%E6%99%9A%EF%BC%81Python%E9%82%A3%E4%BA%9B%E4%B8%8D%E4%B8%BA%E4%BA%BA%E7%9F%A5%E7%9A%84%E5%A4%A7%E5%9D%91/"
    >相见恨晚！Python那些不为人知的大坑.md</a> 
</h2>
 

    </header>
     
    <div class="article-meta">
      <a href="/2020/11/11/develop/%E7%9B%B8%E8%A7%81%E6%81%A8%E6%99%9A%EF%BC%81Python%E9%82%A3%E4%BA%9B%E4%B8%8D%E4%B8%BA%E4%BA%BA%E7%9F%A5%E7%9A%84%E5%A4%A7%E5%9D%91/" class="article-date">
  <time datetime="2020-11-10T16:00:00.000Z" itemprop="datePublished">2020-11-11</time>
</a> 
  <div class="article-category">
    <a class="article-category-link" href="/categories/develop/">develop</a>
  </div>
   
    </div>
      
    <div class="article-entry" itemprop="articleBody">
       
  <h1 id="相见恨晚！Python那些不为人知的大坑"><a href="#相见恨晚！Python那些不为人知的大坑" class="headerlink" title="相见恨晚！Python那些不为人知的大坑"></a>相见恨晚！Python那些不为人知的大坑</h1><p>”Python容易“似乎已经成了很多开发者的共识，的确，它的表达方式已经非常接近自然语言。不用像C++那样去考虑指针、内存，也不用像Java那样深入理解JVM。</p>
<p>慢慢的，开发者开始确切的认为”我们写的代码是完全正确的“。</p>
<p>但是，在忽视Python的细节的过程中，会发现，会出现奇奇怪怪的错误。回头定位时，迟迟定位不出问题，在网络上搜索也找不到对应的解决方案，回头看代码，觉得代码<strong>无懈可击</strong>。殊不知，在开发过程中已经犯了很多错误。</p>
<p>本文，就总结5个Python初学者经常会遇到的错误。不瞒各位，其中有些错误我当初也踩过坑，并且迟迟无法逃脱，希望把这些问题总结出来，避免大家再次踩坑！</p>
<h3 id="1-字典拷贝"><a href="#1-字典拷贝" class="headerlink" title="1. 字典拷贝"></a>1. 字典拷贝</h3><p>有些场景下，需要对字典拷贝一个副本。这个副本用于保存原始数据，然后原来的字典去参与其他运算，或者作为参数传递给一些函数。</p>
<p>例如，</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">&gt;&gt;&gt; dict_a &#x3D; &#123;&quot;name&quot;: &quot;John&quot;, &quot;address&quot;:&quot;221B Baker street&quot;&#125;  </span><br><span class="line">&gt;&gt;&gt; dict_b &#x3D; dict_a  </span><br><span class="line"></span><br></pre></td></tr></table></figure>

<p>利用赋值运算法把<code>dict_a</code>赋值给<code>dict_b</code>之后，这2个变量的值是相同的。</p>
<p>你或许会拿着<code>dict_b</code>去参与其他的运算，例如，更新/添加键值对。</p>
<p>但事实却不是你认为的那样，如果你更新或者编辑<code>dict_b</code>，<code>dict_a</code>也会随之改变，详细内容可以了解一下Python可变对象与不可变对象。</p>
<p>下面来看一下效果：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">&gt;&gt;&gt; dict_b[&quot;age&quot;] &#x3D; 26  </span><br><span class="line">&gt;&gt;&gt; dict_b  </span><br><span class="line">&#123;&#39;address&#39;: &#39;221B Baker street&#39;, &#39;name&#39;: &#39;John&#39;, &#39;age&#39;: 26&#125;  </span><br><span class="line">&gt;&gt;&gt; dict_a  </span><br><span class="line">&#123;&#39;address&#39;: &#39;221B Baker street&#39;, &#39;name&#39;: &#39;John&#39;, &#39;age&#39;: 26&#125;  </span><br><span class="line"></span><br></pre></td></tr></table></figure>

<p>你会发现，给<code>dict_b</code>添加了一个<code>age:26</code>键值对，<code>dict_a</code>也更新了，这样，我们留一个副本就没有任何意义了。</p>
<p>针对这个问题，可以用Python中的浅拷贝<code>copy</code>、深拷贝<code>deepcopy</code>来解决，下面来看一下，</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">&gt;&gt;&gt; dict_c &#x3D; dict_b.copy()  </span><br><span class="line">&gt;&gt;&gt; dict_c[&quot;location&quot;] &#x3D; &quot;somewhere&quot;  </span><br><span class="line">&gt;&gt;&gt; dict_c  </span><br><span class="line">&#123;&#39;address&#39;: &#39;221B Baker street&#39;, &#39;name&#39;: &#39;John&#39;, &#39;age&#39;: 26, &#39;location&#39;: &#39;somewhere&#39;&#125;  </span><br><span class="line">&gt;&gt;&gt; dict_b  </span><br><span class="line">&#123;&#39;address&#39;: &#39;221B Baker street&#39;, &#39;name&#39;: &#39;John&#39;, &#39;age&#39;: 26&#125;  </span><br><span class="line"></span><br></pre></td></tr></table></figure>

<h3 id="2-字典键值"><a href="#2-字典键值" class="headerlink" title="2. 字典键值"></a>2. 字典键值</h3><p>首先，来看一个示例，</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">&gt;&gt;&gt; dict_a &#x3D; dict()  </span><br><span class="line">&gt;&gt;&gt; dict_a  </span><br><span class="line">&#123;&#125;  </span><br><span class="line">&gt;&gt;&gt; dict_a[1] &#x3D; &quot;apple&quot;  </span><br><span class="line">&gt;&gt;&gt; dict_a[True] &#x3D; &quot;mango&quot;  </span><br><span class="line">&gt;&gt;&gt; dict_a[2] &#x3D; &quot;melon&quot;  </span><br><span class="line">&gt;&gt;&gt; dict_a  </span><br><span class="line">&#123;1: &#39;mango&#39;, 2: &#39;melon&#39;&#125;  </span><br><span class="line"></span><br></pre></td></tr></table></figure>

<p>你注意到发生了什么吗？</p>
<p>输出字典之后，发现键值<code>True</code>没有了！</p>
<p>这是因为，在Python中，True相当于1、False相当于0，因此，在<code>dict_a[True] = &quot;mango&quot;</code>这行代码里，它把原来键值为<code>1</code>给替换了。</p>
<p>可以来验证一下True相当于1的说法：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">&gt;&gt;&gt; isinstance(True, int)  </span><br><span class="line">True  </span><br><span class="line">&gt;&gt;&gt; True &#x3D;&#x3D; 1  </span><br><span class="line">True  </span><br><span class="line"></span><br></pre></td></tr></table></figure>

<h3 id="3-更新列表或字典"><a href="#3-更新列表或字典" class="headerlink" title="3. 更新列表或字典"></a>3. 更新列表或字典</h3><p>同样，先看一个列表的示例：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">&gt;&gt;&gt; list_a &#x3D; [1,2,3,4,5]  </span><br><span class="line">&gt;&gt;&gt; list_a &#x3D; list_a.append(6)  </span><br><span class="line">&gt;&gt;&gt; list_a  </span><br><span class="line"># 不输出任何内容  </span><br><span class="line"></span><br></pre></td></tr></table></figure>

<p>再看一个字典的示例：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">&gt;&gt;&gt; dict_a &#x3D; &#123;&quot;a&quot; : &quot;b&quot;&#125;  </span><br><span class="line">&gt;&gt;&gt; dict_a &#x3D; dict_a.update(&#123;&quot;c&quot; : &quot;d&quot;&#125;)  </span><br><span class="line">&gt;&gt;&gt; dict_a  </span><br><span class="line"># 不输出任何内容  </span><br><span class="line"></span><br></pre></td></tr></table></figure>

<p>发现，打印<code>list_a</code>和<code>dict_a</code>竟然没有任何内容输出！</p>
<p>这是因为，在Python中列表、字典中的一些方法，如排序、更新、附加、添加等，不会创建不必要的副本，从而提升性能，因此，不需要重新分配给变量。</p>
<p>再看一下正确的方法：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">&gt;&gt;&gt; list_a &#x3D; [1,2,3,4,5]  </span><br><span class="line">&gt;&gt;&gt; list_a.append(6)  </span><br><span class="line">&gt;&gt;&gt; list_a.sort()  </span><br><span class="line">&gt;&gt;&gt; list_a  </span><br><span class="line">[1, 2, 3, 4, 5, 6]  </span><br><span class="line"></span><br></pre></td></tr></table></figure>

<h3 id="4-驻留字符串"><a href="#4-驻留字符串" class="headerlink" title="4. 驻留字符串"></a>4. 驻留字符串</h3><p>在某些情况下，Python尝试重用现有的不可变对象。字符串驻留就是这样一种情况。</p>
<p>来看一个示例对比，</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">&gt;&gt;&gt; a &#x3D; &quot;gmail&quot;  </span><br><span class="line">&gt;&gt;&gt; b &#x3D; &quot;gmail&quot;  </span><br><span class="line">&gt;&gt;&gt; a is b  </span><br><span class="line">True  </span><br><span class="line"></span><br></pre></td></tr></table></figure>

<p>然后修改一下，</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">&gt;&gt;&gt; a &#x3D; &quot;@gmail&quot;  </span><br><span class="line">&gt;&gt;&gt; b &#x3D; &quot;@gmail&quot;  </span><br><span class="line">&gt;&gt;&gt; a is b  </span><br><span class="line">False  </span><br><span class="line"></span><br></pre></td></tr></table></figure>

<p>是不是很神奇？我们只加了一个<code>@</code>符号，结果却截然不同！</p>
<p>在第一个实现方法中，尝试创建两个不同的字符串对象。但是当检查两个对象是否相同时，它返回True。</p>
<p>这是因为python并没有创建另一个对象b，而是将b指向了第一个值<code>gmail</code>，换句话说它被驻留了。</p>
<p>但是，如果字符串中除ASCII字符、数字、下划线以外的其他字符时，它则不会驻留，这样的话，它就不会再指向<code>@gmail</code>。</p>
<p>这里需要注意一下，<code>is</code>与<code>==</code>的运算是不同的。</p>
<p><code>==</code>用于判断<strong>值</strong>是否相等，<code>is</code>不仅需要值相等，还需要指向同一个对象。</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">&gt;&gt;&gt; a &#x3D; &quot;@gmail&quot;  </span><br><span class="line">&gt;&gt;&gt; b &#x3D; &quot;@gmail&quot;  </span><br><span class="line">&gt;&gt;&gt; a is b  </span><br><span class="line">False  </span><br><span class="line">&gt;&gt;&gt; a &#x3D;&#x3D; b  </span><br><span class="line">True  </span><br><span class="line"></span><br></pre></td></tr></table></figure>

<h3 id="5-默认参数只计算一次"><a href="#5-默认参数只计算一次" class="headerlink" title="5. 默认参数只计算一次"></a>5. 默认参数只计算一次</h3><p>先看一下下面这个示例：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">&gt;&gt;&gt; def func(a, lst&#x3D;[]):  </span><br><span class="line">...     lst.append(a)  </span><br><span class="line">...     return lst  </span><br><span class="line">...   </span><br><span class="line">&gt;&gt;&gt; print(func(1))  </span><br><span class="line">[1]  </span><br><span class="line">&gt;&gt;&gt; print(func(2))  </span><br><span class="line">[1, 2]  </span><br><span class="line"></span><br></pre></td></tr></table></figure>

<p>这里发生了什么？</p>
<p>我在<code>func</code>中给了一个默认参数<code>[]</code>，然后先后调用2次<code>func</code>函数。</p>
<p>按照我们常规的认识，这2次调用是分开的，第1次调用输出<code>[1]</code>，第二次应该输出<code>[2]</code>，为什么第2次调用时列表里竟然还保留着第1次调用时的值？</p>
<p>这是因为，在Python中，默认参数只会被计算一次。第1次调用<code>func(1)</code>时，它用到了默认参数。但是，第2次调用就不会再去计算默认参数，直接在<code>[1]</code>的基础上附加一个值。</p>
<h3 id="结语"><a href="#结语" class="headerlink" title="结语"></a>结语</h3><p>本文介绍的这些经常犯错的点，或许你还没有遇到过，因此会觉得不以为然。但是，总会在某个时间点、开发某个小模块时不知不觉中用到这5个问题所涵盖的其中某个功能。</p>
<p>如果想当然，按照我们概念里认为的那样去做，结果可想而知，会出现错误。</p>
<p>因此，在Python开发过程中，应该注意一些细节，深入系统的学习一下Python，避免在一些Python基本特性方面犯一些错误。</p>
<p>如果你对Python有深入的了解，自然会避免这些错误。反之，则会给你带来大麻烦，一旦出了错误就很难定位出来。</p>
<hr>
<p><strong>往期精选</strong></p>
<p><a target="_blank" rel="noopener" href="http://mp.weixin.qq.com/s?__biz=MzUzNTMwNzUwNg==&mid=2247483920&idx=1&sn=532aeb760280ec13100092725e45ea94&chksm=fa86378dcdf1be9b182b196386a435c7457bdfe4fae8fa85e70a710a75f075f8e12c945c9c24&scene=21#wechat_redirect">太酷了！一款强大的机器学习可视化IDE</a>  </p>
<p><a target="_blank" rel="noopener" href="http://mp.weixin.qq.com/s?__biz=MzUzNTMwNzUwNg==&mid=2247483929&idx=1&sn=b909dda85c9157878d6c3deb758c5a68&chksm=fa863784cdf1be92d56129f4265a7c0087050ece4ee33b87dcebada9abec37ca826d57fab56f&scene=21#wechat_redirect">太好用！教你几招Python魔法方法的妙用</a>  </p>
<p><a target="_blank" rel="noopener" href="http://mp.weixin.qq.com/s?__biz=MzUzNTMwNzUwNg==&mid=2247483909&idx=1&sn=e2cf46cddf0868384815a229213d82f0&chksm=fa863798cdf1be8e853e24bd22dc0e7128b2126eb88d506ac7a2e6467dfe3f12f36f0318f20c&scene=21#wechat_redirect">GitHub万赞！这个神仙资源一定能够让你的Python技能更上一层楼</a>  </p>
<p><strong>福利</strong></p>
<p>最近我花费了半个月的时间，整理了1份理论+实践的计算机视觉入门教程，这或许是你见过最好的一份CV教程之一。独家打造、完全免费，需要的同学可以扫码添加我的个人微信，发送“<strong>CV</strong>”获取~</p>
<p><img src=""></p>
 
      <!-- reward -->
      
    </div>
    

    <!-- copyright -->
    
    <footer class="article-footer">
       
  <ul class="article-tag-list" itemprop="keywords"><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/develop/" rel="tag">develop</a></li></ul>

    </footer>
  </div>

    
 
   
</article>

    
    <article
  id="post-develop/这样的代码才叫 Pythonic"
  class="article article-type-post"
  itemscope
  itemprop="blogPost"
  data-scroll-reveal
>
  <div class="article-inner">
    
    <header class="article-header">
       
<h2 itemprop="name">
  <a class="article-title" href="/2020/11/11/develop/%E8%BF%99%E6%A0%B7%E7%9A%84%E4%BB%A3%E7%A0%81%E6%89%8D%E5%8F%AB%20Pythonic/"
    >这样的代码才叫 Pythonic.md</a> 
</h2>
 

    </header>
     
    <div class="article-meta">
      <a href="/2020/11/11/develop/%E8%BF%99%E6%A0%B7%E7%9A%84%E4%BB%A3%E7%A0%81%E6%89%8D%E5%8F%AB%20Pythonic/" class="article-date">
  <time datetime="2020-11-10T16:00:00.000Z" itemprop="datePublished">2020-11-11</time>
</a> 
  <div class="article-category">
    <a class="article-category-link" href="/categories/develop/">develop</a>
  </div>
   
    </div>
      
    <div class="article-entry" itemprop="articleBody">
       
  <h1 id="这样的代码才叫-Pythonic"><a href="#这样的代码才叫-Pythonic" class="headerlink" title="这样的代码才叫 Pythonic"></a>这样的代码才叫 Pythonic</h1><p>Python由于语言的简洁性，让我们以人类思考的方式来写代码，新手更容易上手，老鸟更爱不释手。</p>
<p>要写出 Pythonic（优雅的、地道的、整洁的）代码，还要平时多观察那些大牛代码，这里明哥收集了一些比较常见的 Pythonic 写法，帮助你养成写优秀代码的习惯。</p>
<p><strong><img src="https://mmbiz.qpic.cn/mmbiz_png/Pn4Sm0RsAugo5SicrbBWhBdrvsmIrWNwHklazcTAqHwNiakxwcrlNUFmx9RjNGxR0ghGKMoO9ubibCib9CHYkkT2HA/640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1"></strong></p>
<h2 id="变量交换"><a href="#变量交换" class="headerlink" title="变量交换"></a><strong>变量交换</strong></h2><p>交换两个变量的值，正常都会想利用一个中间临时变量来过渡。</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"></span><br><span class="line">&#96;tmp &#x3D; a&#96;&#96;a &#x3D; b&#96;&#96;b &#x3D; tmp&#96;</span><br><span class="line"></span><br><span class="line"></span><br></pre></td></tr></table></figure>

<p>能用一行代码解决的（并且不影响可读性的），决不用三行代码。</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"></span><br><span class="line">&#96;a,b &#x3D; b,a&#96;</span><br><span class="line"></span><br><span class="line"></span><br></pre></td></tr></table></figure>



<p><strong><img src="https://mmbiz.qpic.cn/mmbiz_png/Pn4Sm0RsAugo5SicrbBWhBdrvsmIrWNwH79IdgwBuCldKPGDicPyLfzel3DBo8Odgd8VbbYiaUZG3qBz5DRhEbmgg/640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1"></strong></p>
<p><strong>列表推导</strong>  </p>
<hr>
<p>下面是一个非常简单的 for 循环。</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"></span><br><span class="line">&#96;my_list &#x3D; []&#96;&#96;for i in range(10):&#96; &#96;my_list.append(i*2)&#96;</span><br><span class="line"></span><br><span class="line"></span><br></pre></td></tr></table></figure>

<p>在一个 for 循环中，如果逻辑比较简单，不如试用一下列表的列表推导式，虽然只有一行代码，但也逻辑清晰。</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"></span><br><span class="line">&#96;my_list &#x3D; [i*2 for i in range(10)]&#96;</span><br><span class="line"></span><br><span class="line"></span><br></pre></td></tr></table></figure>

<p><strong><img src="https://mmbiz.qpic.cn/mmbiz_png/Pn4Sm0RsAugo5SicrbBWhBdrvsmIrWNwHgvU6jQlC6U6g2F1sBAUicFbcmNibDa0d3vHpawVWfn5654o7uLI7icozw/640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1"></strong></p>
<p><strong>单行表达式</strong>  </p>
<hr>
<p>上面两个案例，都将多行代码用另一种方式写成了一行代码。</p>
<p>这并不意味着，代码行数越少，就越 Pythonic 。</p>
<p>比如下面这样写，就不推荐。</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"></span><br><span class="line">&#96;print(&#39;hello&#39;); print(&#39;world&#39;)&#96;&#96;if x &#x3D;&#x3D; 1: print(&#39;hello,world&#39;)&#96;&#96;if &lt;complex comparison&gt; and &lt;other complex comparison&gt;:&#96; &#96;# do something&#96;</span><br><span class="line"></span><br><span class="line"></span><br></pre></td></tr></table></figure>

<p>建议还是按照如下的写法来</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"></span><br><span class="line">&#96;print（&#39;hello&#39;）&#96;&#96;print（&#39;world&#39;）&#96;&#96;if x &#x3D;&#x3D; 1:&#96; &#96;print(&#39;hello,world&#39;)&#96;&#96;cond1 &#x3D; &lt;complex comparison&gt;&#96;&#96;cond2 &#x3D; &lt;other complex comparison&gt;&#96;&#96;if cond1 and cond2:&#96; &#96;# do something&#96;</span><br><span class="line"></span><br><span class="line"></span><br></pre></td></tr></table></figure>

<p><strong><img src="https://mmbiz.qpic.cn/mmbiz_png/Pn4Sm0RsAugo5SicrbBWhBdrvsmIrWNwHw0gbr2nQhORCNib5Wlbuo2ane4qziancmoHuyPK5NsUThZXXnFbiaLdlA/640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1"></strong></p>
<p><strong>带索引遍历</strong>  </p>
<hr>
<p>使用 for 循环时，如何取得对应的索引，初学者习惯使用 range + len </p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"></span><br><span class="line">&#96;for i in range(len(my_list)):&#96; &#96;print(i, &quot;--&gt;&quot;, my_list[i])&#96;</span><br><span class="line"></span><br><span class="line"></span><br></pre></td></tr></table></figure>

<p>更好的做法是利用 enumerate 这个内置函数</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"></span><br><span class="line">&#96;for i,item in enumerate(my_list):&#96; &#96;print(i, &quot;--&gt;&quot;,item)&#96;</span><br><span class="line"></span><br><span class="line"></span><br></pre></td></tr></table></figure>

<p><strong><img src="https://mmbiz.qpic.cn/mmbiz_png/Pn4Sm0RsAugo5SicrbBWhBdrvsmIrWNwH3Njic1JR7J9liafR0Y2TeXIwWZaJPOumK6TLrVGt0Vue5ibQU14iaghCBA/640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1"></strong></p>
<p><strong>序列解包</strong>  </p>
<hr>
<p>使用 * 可以对一个列表解包</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"></span><br><span class="line">&#96;a, *rest &#x3D; [1, 2, 3]&#96;&#96;# a &#x3D; 1, rest &#x3D; [2, 3]&#96;&#96;a, *middle, c &#x3D; [1, 2, 3, 4]&#96;&#96;# a &#x3D; 1, middle &#x3D; [2, 3], c &#x3D; 4&#96;</span><br><span class="line"></span><br><span class="line"></span><br></pre></td></tr></table></figure>

<p><strong><img src="https://mmbiz.qpic.cn/mmbiz_png/Pn4Sm0RsAugo5SicrbBWhBdrvsmIrWNwHyV2C0tibEjhEial4SrXA0YZXaZia8UFjhXbCGgUcn3Yja2kl8ByTmM38w/640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1"></strong></p>
<p><strong>字符串拼接</strong>  </p>
<hr>
<p>如果一个列表（或者可迭代对象）中的所有元素都是字符串对象，想要将他们连接起来，通常做法是</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"></span><br><span class="line">&#96;letters &#x3D; [&#39;s&#39;, &#39;p&#39;, &#39;a&#39;, &#39;m&#39;]&#96;&#96;s&#x3D;&quot;&quot;&#96;&#96;for let in letters:&#96; &#96;s +&#x3D; let&#96;</span><br><span class="line"></span><br><span class="line"></span><br></pre></td></tr></table></figure>

<p>更推荐的做法是使用 join 函数</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"></span><br><span class="line">&#96;letters &#x3D; [&#39;s&#39;, &#39;p&#39;, &#39;a&#39;, &#39;m&#39;]&#96;&#96;word &#x3D; &#39;&#39;.join(letters)&#96;</span><br><span class="line"></span><br><span class="line"></span><br></pre></td></tr></table></figure>

<p><strong><img src="https://mmbiz.qpic.cn/mmbiz_png/Pn4Sm0RsAugo5SicrbBWhBdrvsmIrWNwHCibcNLkqr1tK72ibibS0kvbO9NbItSYoENiam42pAPz5eAebt3AU8ricdfQ/640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1"></strong></p>
<p><strong>真假判断</strong>  </p>
<hr>
<p>判断一个变量是否为真（假），新手习惯直接使用 == 与 True、False、None 进行对比</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"></span><br><span class="line">&#96;if attr &#x3D;&#x3D; True:&#96; &#96;print(&#39;True!&#39;)&#96;&#96;if attr &#x3D;&#x3D; None:&#96; &#96;print(&#39;attr is None!&#39;)&#96;</span><br><span class="line"></span><br><span class="line"></span><br></pre></td></tr></table></figure>

<p>实际上，**””、[]、{}** 这些没有任何元素的容器都是假值，可直接使用 <strong>if not xx</strong> 来判断。</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"></span><br><span class="line">&#96;if attr:&#96; &#96;print(&#39;attr is truthy!&#39;)&#96;&#96;if not attr:&#96; &#96;print(&#39;attr is falsey!&#39;)&#96;</span><br><span class="line"></span><br><span class="line"></span><br></pre></td></tr></table></figure>

<p><strong><img src="https://mmbiz.qpic.cn/mmbiz_png/Pn4Sm0RsAugo5SicrbBWhBdrvsmIrWNwH9gMe7RFicNoE1XfuIANfyphuYhTLAjZ5OW2gwU2NqySzBlMxEX5hPxA/640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1"></strong></p>
<p><strong>访问字典元素</strong>  </p>
<hr>
<p>当直接使用 <strong>[]</strong> 来访问字典里的元素时，若key不存在，是会抛异常的，所以新会可能会先判断一下是否有这个 key，有再取</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"></span><br><span class="line">&#96;d &#x3D; &#123;&#39;hello&#39;: &#39;world&#39;&#125;&#96;&#96;if d.has_key(&#39;hello&#39;):&#96; &#96;print(d[&#39;hello&#39;])    # prints &#39;world&#39;&#96;&#96;else:&#96; &#96;print(&#39;default_value&#39;)&#96;</span><br><span class="line"></span><br><span class="line"></span><br></pre></td></tr></table></figure>

<p>更推荐的做法是使用 <strong>get</strong> 来取，如果没有该 key 会默认返回 None（当然你也可以设置默认返回值）</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"></span><br><span class="line">&#96;d &#x3D; &#123;&#39;hello&#39;: &#39;world&#39;&#125;&#96;&#96;print(d.get(&#39;hello&#39;, &#39;default_value&#39;)) # prints &#39;world&#39;&#96;&#96;print(d.get(&#39;thingy&#39;, &#39;default_value&#39;)) # prints &#39;default_value&#39;&#96;</span><br><span class="line"></span><br><span class="line"></span><br></pre></td></tr></table></figure>

<p><strong><img src="https://mmbiz.qpic.cn/mmbiz_png/Pn4Sm0RsAugo5SicrbBWhBdrvsmIrWNwH5Z3BhtxFYWl1gjYae3G8pdEXRYTbP5xf4r9ULlva0BicoPHRyCvYDnQ/640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1"></strong></p>
<p><strong>操作列表</strong>  </p>
<hr>
<p>下面这段代码，会根据条件过滤过列表中的元素</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"></span><br><span class="line">&#96;a &#x3D; [3, 4, 5]&#96;&#96;b &#x3D; []&#96;&#96;for i in a:&#96; &#96;if i &gt; 4:&#96; &#96;b.append(i)&#96;</span><br><span class="line"></span><br><span class="line"></span><br></pre></td></tr></table></figure>

<p>实际上可以使用列表推导或者高阶函数 filter 来实现</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"></span><br><span class="line">&#96;a &#x3D; [3, 4, 5]&#96;&#96;b &#x3D; [i for i in a if i &gt; 4]&#96;&#96;# Or:&#96;&#96;b &#x3D; filter(lambda x: x &gt; 4, a)&#96;</span><br><span class="line"></span><br><span class="line"></span><br></pre></td></tr></table></figure>

<p>除了 filter 之外，还有 map、reduce 这两个函数也很好用</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"></span><br><span class="line">&#96;a &#x3D; [3, 4, 5]&#96;&#96;b &#x3D; map(lambda i: i + 3, a)&#96;&#96;# b: [6,7,8]&#96;</span><br><span class="line"></span><br><span class="line"></span><br></pre></td></tr></table></figure>

<p><strong><img src="https://mmbiz.qpic.cn/mmbiz_png/Pn4Sm0RsAugo5SicrbBWhBdrvsmIrWNwHJQ6HeBxl8dibSmCKpWZ2uzInicfCsNh4odicLa1kZibQicVvORicZWWl4KfA/640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1"></strong></p>
<p><strong>文件读取</strong>  </p>
<hr>
<p>文件读取是非常常用的操作，在使用完句柄后，是需要手动调用 close 函数来关闭句柄的</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"></span><br><span class="line">&#96;fp &#x3D; open(&#39;file.txt&#39;)&#96;&#96;print(fp.read())&#96;&#96;fp.close()&#96;</span><br><span class="line"></span><br><span class="line"></span><br></pre></td></tr></table></figure>

<p>如果代码写得太长，即使你知道需要手动关闭句柄，却也会经常会漏掉。因此推荐养成习惯使用 <strong>with open</strong> 来读写文件，上下文管理器会自动执行关闭句柄的操作</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"></span><br><span class="line">&#96;with open(&#39;file.txt&#39;) as fp:&#96; &#96;for line in fp.readlines():&#96; &#96;print(line)&#96;</span><br><span class="line"></span><br><span class="line"></span><br></pre></td></tr></table></figure>

<p><strong><img src="https://mmbiz.qpic.cn/mmbiz_png/Pn4Sm0RsAugo5SicrbBWhBdrvsmIrWNwHicyS5CI0JQ512B81UEIfGcibAQJ8E2IdosuZIy5ezKsEswbwmt60xOww/640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1"></strong></p>
<p><strong>代码续行</strong>  </p>
<hr>
<p>将一个长度较长的字符串放在一行中，是很影响代码可读性的(下面代码可向左滑动)</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"></span><br><span class="line">&#96;long_string &#x3D; &#39;For a long time I used to go to bed early. Sometimes, when I had put out my candle, my eyes would close so quickly that I had not even time to say “I’m going to sleep.”&#39;&#96;</span><br><span class="line"></span><br><span class="line"></span><br></pre></td></tr></table></figure>

<p>稍等注重代码可读性的人，会使用三个引号 <strong>\</strong>来续写</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"></span><br><span class="line">&#96;long_string &#x3D; &#39;For a long time I used to go to bed early. &#39; \&#96; &#96;&#39;Sometimes, when I had put out my candle, &#39; \&#96; &#96;&#39;my eyes would close so quickly that I had not even time to say “I’m going to sleep.”&#39;&#96;</span><br><span class="line"></span><br><span class="line"></span><br></pre></td></tr></table></figure>

<p>不过，对我来说，我更喜欢这样子写 使用括号包裹 <strong>()</strong></p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"></span><br><span class="line">&#96;long_string &#x3D; (&#96; &#96;&quot;For a long time I used to go to bed early. Sometimes, &quot;&#96; &#96;&quot;when I had put out my candle, my eyes would close so quickly &quot;&#96; &#96;&quot;that I had not even time to say “I’m going to sleep.”&quot;&#96;&#96;)&#96;</span><br><span class="line"></span><br><span class="line"></span><br></pre></td></tr></table></figure>

<p>导包的时候亦是如此</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"></span><br><span class="line">&#96;from some.deep.module.inside.a.module import (&#96; &#96;a_nice_function, another_nice_function, yet_another_nice_function)&#96;</span><br><span class="line"></span><br><span class="line"></span><br></pre></td></tr></table></figure>

<p><strong><img src="https://mmbiz.qpic.cn/mmbiz_png/Pn4Sm0RsAugo5SicrbBWhBdrvsmIrWNwHjr6ib80KaLEfiaWCUrW5CRvFtOavAU4DXSTuzeyJqnOE6ibSkG1AWJpZg/640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1"></strong></p>
<p><strong>显式代码</strong>  </p>
<hr>
<p>有时候出于需要，我们会使用一些特殊的魔法来使代码适应更多的场景不确定性。</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"></span><br><span class="line">&#96;def make_complex(*args):&#96; &#96;x, y &#x3D; args&#96; &#96;return dict(**locals())&#96;</span><br><span class="line"></span><br><span class="line"></span><br></pre></td></tr></table></figure>

<p>但若非必要，请不要那么做。无端增加代码的不确定性，会让原先本就动态的语言写出更加动态的代码。</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"></span><br><span class="line">&#96;def make_complex(x, y):&#96; &#96;return &#123;&#39;x&#39;: x, &#39;y&#39;: y&#125;&#96;</span><br><span class="line"></span><br><span class="line"></span><br></pre></td></tr></table></figure>

<p><strong><img src="https://mmbiz.qpic.cn/mmbiz_png/Pn4Sm0RsAugo5SicrbBWhBdrvsmIrWNwH01WELfAMUxxQ4NkFJiaHw6JWWNkDCASzsIcMxSbt6koC6qu2xx3boNw/640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1"></strong></p>
<p><strong>使用占位符</strong>  </p>
<hr>
<p>对于暂不需要，却又不得不接收的的变量，请使用占位符</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"></span><br><span class="line">&#96;filename &#x3D; &#39;foobar.txt&#39;&#96;&#96;basename, _, ext &#x3D; filename.rpartition(&#39;.&#39;)&#96;</span><br><span class="line"></span><br><span class="line"></span><br></pre></td></tr></table></figure>

<p><strong><img src="https://mmbiz.qpic.cn/mmbiz_png/Pn4Sm0RsAugo5SicrbBWhBdrvsmIrWNwHfLciaCqaEYJwHeibJjDoCfAdib5RZ8ib3tabKapO4G0ZRUuhXeGe9ibXbRA/640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1"></strong></p>
<p><strong>链式比较</strong>  </p>
<hr>
<p>对于下面这种写法</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"></span><br><span class="line">&#96;score &#x3D; 85&#96;&#96;if score &gt; 80 and score &lt; 90:&#96; &#96;print(&quot;良好&quot;)&#96;</span><br><span class="line"></span><br><span class="line"></span><br></pre></td></tr></table></figure>

<p>其实还有更好的写法</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"></span><br><span class="line">&#96;score &#x3D; 85&#96;&#96;if 80 &lt; score &lt; 90:&#96; &#96;print(&quot;良好&quot;)&#96;</span><br><span class="line"></span><br><span class="line"></span><br></pre></td></tr></table></figure>

<p>如果你理解了上面的链式比较操作，那么你应该知道为什么下面这行代码输出的结果是 False</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"></span><br><span class="line">&#96;&gt;&gt;&gt; False &#x3D;&#x3D; False &#x3D;&#x3D; True&#96; &#96;False&#96;</span><br><span class="line"></span><br><span class="line"></span><br></pre></td></tr></table></figure>

<p><strong><img src="https://mmbiz.qpic.cn/mmbiz_png/Pn4Sm0RsAugo5SicrbBWhBdrvsmIrWNwHQ0nQgF9YLYkvAdvw4LkjAovg7MW664ZejVJtBa4hY7167m9shlaJSQ/640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1"></strong></p>
<p><strong>三目运算</strong>  </p>
<hr>
<p>对于简单的判断并赋值</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"></span><br><span class="line">&#96;age &#x3D; 20&#96;&#96;if age &gt; 18:&#96; &#96;type &#x3D; &quot;adult&quot;&#96;&#96;else:&#96; &#96;type &#x3D; &quot;teenager&quot;&#96;</span><br><span class="line"></span><br><span class="line"></span><br></pre></td></tr></table></figure>

<p>其实是可以使用三目运算，一行搞定。</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">age &#x3D; 20  b &#x3D; &quot;adult&quot; if age &gt; 18 else &quot;teenager&quot;</span><br></pre></td></tr></table></figure>



<p><img src="https://mmbiz.qpic.cn/mmbiz_png/1hReHaqafafJYoFH7OAhmUdTjo35vvJTy1lRVjG2CzFP3arfVRDqI7a8PSS6Sx5LialaFTE1HFu2N4OIhL8jP9g/640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1"></p>
<p><img src="https://mmbiz.qpic.cn/mmbiz_jpg/Pn4Sm0RsAuge5LksSBIebanurzZbauYRUiciaSLZDju7lfe7HrEEmaTZFz52ASkiaK35qv36IpPo9IbTBtkWO4MJg/640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1"></p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"></span><br></pre></td></tr></table></figure>


<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"></span><br><span class="line">**更多精彩推荐**</span><br><span class="line"></span><br></pre></td></tr></table></figure>


<p><a target="_blank" rel="noopener" href="http://mp.weixin.qq.com/s?__biz=MjM5MjAwODM4MA==&mid=2650788258&idx=1&sn=8c8120bba90e3f1d9a3ad7411f33ace0&chksm=bea7a07189d02967fc29cac18a8074e6b778417338693aa7fd030333c42ba987316af537b4b0&scene=21#wechat_redirect">☞</a><a target="_blank" rel="noopener" href="http://mp.weixin.qq.com/s?__biz=MjM5MjAwODM4MA==&mid=2650788258&idx=1&sn=8c8120bba90e3f1d9a3ad7411f33ace0&chksm=bea7a07189d02967fc29cac18a8074e6b778417338693aa7fd030333c42ba987316af537b4b0&scene=21#wechat_redirect">机器人也开始”怕疼”了？科学家开发无需人工干预即可”自愈”的机器人  
</a></p>
<p><a target="_blank" rel="noopener" href="http://mp.weixin.qq.com/s?__biz=MjM5MjAwODM4MA==&mid=2650788258&idx=1&sn=8c8120bba90e3f1d9a3ad7411f33ace0&chksm=bea7a07189d02967fc29cac18a8074e6b778417338693aa7fd030333c42ba987316af537b4b0&scene=21#wechat_redirect">☞</a><a target="_blank" rel="noopener" href="http://mp.weixin.qq.com/s?__biz=MjM5MjAwODM4MA==&mid=2650791237&idx=1&sn=000a920dd1d7c92b015137ecf0eb981b&chksm=bea7ac9689d02580c2bc20812405ec5ed2e7414837370f4a3d1070ad58f21a2a505c87db714e&scene=21#wechat_redirect">对话指令集创始人兼CEO潘爱民：面向未来的新型物联网操作系统 | 人物志</a></p>
<p><a target="_blank" rel="noopener" href="http://mp.weixin.qq.com/s?__biz=MjM5MjAwODM4MA==&mid=2650754382&idx=3&sn=7774fe80d79901abfa01500971230e84&chksm=bea73c9d89d0b58bbca823368137198834d3c4300f00d11917ac93a6dbaabb8378813c66a0a5&scene=21#wechat_redirect">☞</a><a target="_blank" rel="noopener" href="http://mp.weixin.qq.com/s?__biz=MzA5MzY4NTQwMA==&mid=2651033873&idx=1&sn=a72783457162a649a60c4270e94eb491&chksm=8bad22e6bcdaabf01e7aaba171934c4a70d7e820504aa695ca6b8f9ee95a45a85be93a91e535&scene=21#wechat_redirect">@程序员，什么才是“2020-1024”的正确打开姿势？</a>  </p>
<p><a target="_blank" rel="noopener" href="http://mp.weixin.qq.com/s?__biz=MjM5MjAwODM4MA==&mid=2650750309&idx=2&sn=e6772d93425a7a23a9d53c793274b9bd&chksm=bea74cb689d0c5a0a5958c2f20aaa0fdf3616a3089b6e61082e47f6639ed593951c7d39e5698&scene=21#wechat_redirect">☞</a><a target="_blank" rel="noopener" href="http://mp.weixin.qq.com/s?__biz=MzI0ODcxODk5OA==&mid=2247529015&idx=1&sn=5713c9afae9c8be69fe0a657f7746d76&chksm=e99e41cedee9c8d8a518fa664adb4a4475ba6e32db882805093d91c2a94c0c4b6fb17cece178&scene=21#wechat_redirect">最新！百度首发 OCR 自训练平台 EasyDL OCR</a>  </p>
<p><a target="_blank" rel="noopener" href="http://mp.weixin.qq.com/s?__biz=MjM5MjAwODM4MA==&mid=2650746260&idx=1&sn=ec250a1510b9cf57c8ba19578decefb1&chksm=bea75c4789d0d551e79436a31b723defb9f792c09c0d78c83d04b011a5c1758175aad851c534&scene=21#wechat_redirect">☞</a><a target="_blank" rel="noopener" href="http://mp.weixin.qq.com/s?__biz=MzA3MjY1MTQwNQ==&mid=2649844744&idx=1&sn=8f4144a8e0720a61f6f87eb80ee00335&chksm=871ecbeab06942fc62da9c230d221a13eb74ff9eaeb4a32a9cc2d9af29e847b1c8b718441dd4&scene=21#wechat_redirect">SQL分页查询方案的性能对比</a></p>
<p>☞<a target="_blank" rel="noopener" href="http://mp.weixin.qq.com/s?__biz=MzU2MTE1NDk2Mg==&mid=2247507375&idx=1&sn=db44a14e7e1497987501df7557820e65&chksm=fc7f8352cb080a4489f7f110f5e28fcd2948b4fa870fdfc462374422aaf04d91706e440f97db&scene=21#wechat_redirect">2021年，很可能是以太坊的“高光之年”</a></p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"></span><br></pre></td></tr></table></figure>


<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"></span><br></pre></td></tr></table></figure>

<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"></span><br><span class="line">![](https:&#x2F;&#x2F;mmbiz.qpic.cn&#x2F;mmbiz_png&#x2F;Pn4Sm0RsAugOHoUicBekr3guXbjm0KkicU7iaibnuR7Z4qPYHp4LUBHicYeBx3p5H5cKyCibSSapo9b0RjPficFdgOqpg&#x2F;640?wx_fmt&#x3D;png&amp;tp&#x3D;webp&amp;wxfrom&#x3D;5&amp;wx_lazy&#x3D;1&amp;wx_co&#x3D;1)</span><br><span class="line"></span><br><span class="line">点分享</span><br><span class="line"></span><br><span class="line">![](https:&#x2F;&#x2F;mmbiz.qpic.cn&#x2F;mmbiz_png&#x2F;Pn4Sm0RsAugOHoUicBekr3guXbjm0KkicU4YvCLQDSXxzNJAgPia8JwX0micmkOrs4G1wmf3LOcnNl5nYrqEZud7uQ&#x2F;640?wx_fmt&#x3D;png&amp;tp&#x3D;webp&amp;wxfrom&#x3D;5&amp;wx_lazy&#x3D;1&amp;wx_co&#x3D;1)</span><br><span class="line"></span><br><span class="line">点点赞</span><br><span class="line"></span><br><span class="line">![](https:&#x2F;&#x2F;mmbiz.qpic.cn&#x2F;mmbiz_png&#x2F;Pn4Sm0RsAugOHoUicBekr3guXbjm0KkicUuEaCBibBecgM1vBvth2SicVCYrPVd6d8x2H3lALg3fZnO9du4c91OwKQ&#x2F;640?wx_fmt&#x3D;png&amp;tp&#x3D;webp&amp;wxfrom&#x3D;5&amp;wx_lazy&#x3D;1&amp;wx_co&#x3D;1)</span><br><span class="line"></span><br><span class="line">点在看</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br></pre></td></tr></table></figure> 
      <!-- reward -->
      
    </div>
    

    <!-- copyright -->
    
    <footer class="article-footer">
       
  <ul class="article-tag-list" itemprop="keywords"><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/develop/" rel="tag">develop</a></li></ul>

    </footer>
  </div>

    
 
   
</article>

    
    <article
  id="post-django/10-RESTful架构和DRF进阶"
  class="article article-type-post"
  itemscope
  itemprop="blogPost"
  data-scroll-reveal
>
  <div class="article-inner">
    
    <header class="article-header">
       
<h2 itemprop="name">
  <a class="article-title" href="/2020/11/11/django/10-RESTful%E6%9E%B6%E6%9E%84%E5%92%8CDRF%E8%BF%9B%E9%98%B6/"
    >10-RESTful架构和DRF进阶.md</a> 
</h2>
 

    </header>
     
    <div class="article-meta">
      <a href="/2020/11/11/django/10-RESTful%E6%9E%B6%E6%9E%84%E5%92%8CDRF%E8%BF%9B%E9%98%B6/" class="article-date">
  <time datetime="2020-11-10T16:00:00.000Z" itemprop="datePublished">2020-11-11</time>
</a> 
  <div class="article-category">
    <a class="article-category-link" href="/categories/django/">django</a>
  </div>
   
    </div>
      
    <div class="article-entry" itemprop="articleBody">
       
  <h2 id="RESTful架构和DRF进阶"><a href="#RESTful架构和DRF进阶" class="headerlink" title="RESTful架构和DRF进阶"></a>RESTful架构和DRF进阶</h2><p>除了上一节讲到的方法，使用DRF创建REST风格的数据接口也可以通过CBV（基于类的视图）的方式。使用CBV创建数据接口的特点是代码简单，开发效率高，但是没有FBV（基于函数的视图）灵活，因为使用FBV的方式，数据接口对应的视图函数执行什么样的代码以及返回什么的数据是高度可定制的。下面我们以定制学科的数据接口为例，讲解通过CBV方式定制数据接口的具体做法。</p>
<h3 id="使用CBV"><a href="#使用CBV" class="headerlink" title="使用CBV"></a>使用CBV</h3><h4 id="继承APIView的子类"><a href="#继承APIView的子类" class="headerlink" title="继承APIView的子类"></a>继承APIView的子类</h4><p>修改之前项目中的<code>polls/views.py</code>，去掉<code>show_subjects</code>视图函数，添加一个名为<code>SubjectView</code>的类，该类继承自<code>ListAPIView</code>，<code>ListAPIView</code>能接收GET请求，它封装了获取数据列表并返回JSON数据的<code>get</code>方法。<code>ListAPIView</code>是<code>APIView</code> 的子类，<code>APIView</code>还有很多的子类，例如<code>CreateAPIView</code>可以支持POST请求，<code>UpdateAPIView</code>可以支持PUT和PATCH请求，<code>DestoryAPIView</code>可以支持DELETE请求。<code>SubjectView</code> 的代码如下所示。</p>
<figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">from</span> rest_framework.generics <span class="keyword">import</span> ListAPIView</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">SubjectView</span>(<span class="params">ListAPIView</span>):</span></span><br><span class="line">    <span class="comment"># 通过queryset指定如何获取学科数据</span></span><br><span class="line">    queryset = Subject.objects.all()</span><br><span class="line">    <span class="comment"># 通过serializer_class指定如何序列化学科数据</span></span><br><span class="line">    serializer_class = SubjectSerializer</span><br></pre></td></tr></table></figure>

<p>刚才说过，由于<code>SubjectView</code>的父类<code>ListAPIView</code>已经实现了<code>get</code>方法来处理获取学科列表的GET请求，所以我们只需要声明如何获取学科数据以及如何序列化学科数据，前者用<code>queryset</code>属性指定，后者用<code>serializer_class</code>属性指定。要使用上面的<code>SubjectView</code>，需要修改<code>urls.py</code>文件，如下所示。</p>
<figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">urlpatterns = [</span><br><span class="line">    path(<span class="string">&#x27;api/subjects/&#x27;</span>, SubjectView.as_view()),   </span><br><span class="line">]</span><br></pre></td></tr></table></figure>

<p>很显然，上面的做法较之之前讲到的FBV要简单很多。</p>
<h4 id="继承ModelViewSet"><a href="#继承ModelViewSet" class="headerlink" title="继承ModelViewSet"></a>继承ModelViewSet</h4><p>如果学科对应的数据接口需要支持GET、POST、PUT、PATCH、DELETE请求来支持对学科资源的获取、新增、更新、删除操作，更为简单的做法是继承<code>ModelViewSet</code>来编写学科视图类。再次修改<code>polls/views.py</code>文件，去掉<code>SubjectView</code>类，添加一个名为<code>SubjectViewSet</code>的类，代码如下所示。</p>
<figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">from</span> rest_framework.viewsets <span class="keyword">import</span> ModelViewSet</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">SubjectViewSet</span>(<span class="params">ModelViewSet</span>):</span></span><br><span class="line">    queryset = Subject.objects.all()</span><br><span class="line">    serializer_class = SubjectSerializer</span><br></pre></td></tr></table></figure>

<p>通过查看<code>ModelViewSet</code>类的源代码可以发现，该类共有6个父类，其中前5个父类分别实现对POST（新增学科）、GET（获取指定学科）、PUT/PATCH（更新学科）、DELETE（删除学科）和GET（获取学科列表）操作的支持，对应的方法分别是<code>create</code>、<code>retrieve</code>、<code>update</code>、<code>destroy</code>和<code>list</code>。由于<code>ModelViewSet</code>的父类中已经实现了这些方法，所以我们几乎没有编写任何代码就完成了学科数据全套接口的开发，我们要做的仅仅是指出如何获取到数据（通过<code>queryset</code>属性指定）以及如何序列化数据（通过<code>serializer_class</code>属性指定），这一点跟上面继承<code>APIView</code>的子类做法是一致的。</p>
<figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">ModelViewSet</span>(<span class="params">mixins.CreateModelMixin,</span></span></span><br><span class="line"><span class="class"><span class="params">                   mixins.RetrieveModelMixin,</span></span></span><br><span class="line"><span class="class"><span class="params">                   mixins.UpdateModelMixin,</span></span></span><br><span class="line"><span class="class"><span class="params">                   mixins.DestroyModelMixin,</span></span></span><br><span class="line"><span class="class"><span class="params">                   mixins.ListModelMixin,</span></span></span><br><span class="line"><span class="class"><span class="params">                   GenericViewSet</span>):</span></span><br><span class="line">    <span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">    A viewset that provides default `create()`, `retrieve()`, `update()`,</span></span><br><span class="line"><span class="string">    `partial_update()`, `destroy()` and `list()` actions.</span></span><br><span class="line"><span class="string">    &quot;&quot;&quot;</span></span><br><span class="line">    <span class="keyword">pass</span></span><br></pre></td></tr></table></figure>

<p>要使用上面的<code>SubjectViewSet</code>，需要在<code>urls.py</code>文件中进行URL映射。由于<code>ModelViewSet</code>相当于是多个视图函数的汇总，所以不同于之前映射URL的方式，我们需要先创建一个路由器并通过它注册<code>SubjectViewSet</code>，然后将注册成功后生成的URL一并添加到<code>urlspattern</code>列表中，代码如下所示。</p>
<figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">from</span> rest_framework.routers <span class="keyword">import</span> DefaultRouter</span><br><span class="line"></span><br><span class="line">router = DefaultRouter()</span><br><span class="line">router.register(<span class="string">&#x27;api/subjects&#x27;</span>, SubjectViewSet)</span><br><span class="line">urlpatterns += router.urls</span><br></pre></td></tr></table></figure>

<p>除了<code>ModelViewSet</code>类外，DRF还提供了一个名为<code>ReadOnlyModelViewSet</code> 的类，从名字上就可以看出，该类是只读视图的集合，也就意味着，继承该类定制的数据接口只能支持GET请求，也就是获取单个资源和资源列表的请求。</p>
<h3 id="数据分页"><a href="#数据分页" class="headerlink" title="数据分页"></a>数据分页</h3><p>在使用GET请求获取资源列表时，我们通常不会一次性的加载所有的数据，除非数据量真的很小。大多数获取资源列表的操作都支持数据分页展示，也就说我们可以通过指定页码（或类似于页码的标识）和页面大小（一次加载多少条数据）来获取不同的数据。我们可以通过对<code>QuerySet</code>对象的切片操作来实现分页，也可以利用Django框架封装的<code>Paginator</code>和<code>Page</code>对象来实现分页。使用DRF时，可以在Django配置文件中修改<code>REST_FRAMEWORK</code>并配置默认的分页类和页面大小来实现分页，如下所示。</p>
<figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">REST_FRAMEWORK = &#123;</span><br><span class="line">    <span class="string">&#x27;PAGE_SIZE&#x27;</span>: <span class="number">10</span>,</span><br><span class="line">    <span class="string">&#x27;DEFAULT_PAGINATION_CLASS&#x27;</span>: <span class="string">&#x27;rest_framework.pagination.PageNumberPagination&#x27;</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>除了上面配置的<code>PageNumberPagination</code>分页器之外，DRF还提供了<code>LimitOffsetPagination</code>和<code>CursorPagination</code>分页器，值得一提的是<code>CursorPagination</code>，它可以避免使用页码分页时暴露网站的数据体量，有兴趣的读者可以自行了解。如果不希望使用配置文件中默认的分页设定，可以在视图类中添加一个<code>pagination_class</code>属性来重新指定分页器，通常可以将该属性指定为自定义的分页器，如下所示。</p>
<figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">from</span> rest_framework.pagination <span class="keyword">import</span> PageNumberPagination</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">CustomizedPagination</span>(<span class="params">PageNumberPagination</span>):</span></span><br><span class="line">    <span class="comment"># 默认页面大小</span></span><br><span class="line">    page_size = <span class="number">5</span></span><br><span class="line">    <span class="comment"># 页面大小对应的查询参数</span></span><br><span class="line">    page_size_query_param = <span class="string">&#x27;size&#x27;</span></span><br><span class="line">    <span class="comment"># 页面大小的最大值</span></span><br><span class="line">    max_page_size = <span class="number">50</span></span><br></pre></td></tr></table></figure>

<figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">SubjectView</span>(<span class="params">ListAPIView</span>):</span></span><br><span class="line">    <span class="comment"># 指定如何获取数据</span></span><br><span class="line">    queryset = Subject.objects.all()</span><br><span class="line">    <span class="comment"># 指定如何序列化数据</span></span><br><span class="line">    serializer_class = SubjectSerializer</span><br><span class="line">    <span class="comment"># 指定如何分页</span></span><br><span class="line">    pagination_class = CustomizedPagination</span><br></pre></td></tr></table></figure>

<p>如果不希望数据分页，可以将<code>pagination_class</code>属性设置为<code>None</code>来取消默认的分页器。</p>
<h3 id="数据筛选"><a href="#数据筛选" class="headerlink" title="数据筛选"></a>数据筛选</h3><p>如果希望使用CBV定制获取老师信息的数据接口，也可以通过继承<code>ListAPIView</code>来实现。但是因为要通过指定的学科来获取对应的老师信息，因此需要对老师数据进行筛选而不是直接获取所有老师的数据。如果想从请求中获取学科编号并通过学科编号对老师进行筛选，可以通过重写<code>get_queryset</code>方法来做到，代码如下所示。</p>
<figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">TeacherView</span>(<span class="params">ListAPIView</span>):</span></span><br><span class="line">    serializer_class = TeacherSerializer</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">get_queryset</span>(<span class="params">self</span>):</span></span><br><span class="line">        queryset = Teacher.objects.defer(<span class="string">&#x27;subject&#x27;</span>)</span><br><span class="line">        <span class="keyword">try</span>:</span><br><span class="line">            sno = self.request.GET.get(<span class="string">&#x27;sno&#x27;</span>, <span class="string">&#x27;&#x27;</span>)</span><br><span class="line">            queryset = queryset.filter(subject__no=sno)</span><br><span class="line">            <span class="keyword">return</span> queryset</span><br><span class="line">        <span class="keyword">except</span> ValueError:</span><br><span class="line">            <span class="keyword">raise</span> Http404(<span class="string">&#x27;No teachers found.&#x27;</span>)</span><br></pre></td></tr></table></figure>

<p>除了上述方式之外，还可以使用三方库<code>django-filter</code>来配合DRF实现对数据的筛选，使用<code>django-filter</code>后，可以通过为视图类配置<code>filter-backends</code>属性并指定使用<code>DjangoFilterBackend</code>来支持数据筛选。在完成上述配置后，可以使用<code>filter_fields</code> 属性或<code>filterset_class</code>属性来指定如何筛选数据，有兴趣的读者可以自行研究。��者可以自行研究。</p>
 
      <!-- reward -->
      
    </div>
    

    <!-- copyright -->
    
    <footer class="article-footer">
       
  <ul class="article-tag-list" itemprop="keywords"><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/django/" rel="tag">django</a></li></ul>

    </footer>
  </div>

    
 
   
</article>

    
    <article
  id="post-django/11-使用缓存"
  class="article article-type-post"
  itemscope
  itemprop="blogPost"
  data-scroll-reveal
>
  <div class="article-inner">
    
    <header class="article-header">
       
<h2 itemprop="name">
  <a class="article-title" href="/2020/11/11/django/11-%E4%BD%BF%E7%94%A8%E7%BC%93%E5%AD%98/"
    >11-使用缓存.md</a> 
</h2>
 

    </header>
     
    <div class="article-meta">
      <a href="/2020/11/11/django/11-%E4%BD%BF%E7%94%A8%E7%BC%93%E5%AD%98/" class="article-date">
  <time datetime="2020-11-10T16:00:00.000Z" itemprop="datePublished">2020-11-11</time>
</a> 
  <div class="article-category">
    <a class="article-category-link" href="/categories/django/">django</a>
  </div>
   
    </div>
      
    <div class="article-entry" itemprop="articleBody">
       
  <h2 id="使用缓存"><a href="#使用缓存" class="headerlink" title="使用缓存"></a>使用缓存</h2><p>通常情况下，Web应用的性能瓶颈都会出现在关系型数据库上，当并发访问量较大时，如果所有的请求都需要通过关系型数据库完成数据持久化操作，那么数据库一定会不堪重负。优化Web应用性能最为重要的一点就是使用缓存，把那些数据体量不大但访问频率非常高的数据提前加载到缓存服务器中，这又是典型的空间换时间的方法。通常缓存服务器都是直接将数据置于内存中而且使用了非常高效的数据存取策略（哈希存储、键值对方式等），在读写性能上远远优于关系型数据库的，因此我们可以让Web应用接入缓存服务器来优化其性能，其中一个非常好的选择就是使用Redis。</p>
<p>Web应用的缓存架构大致如下图所示。</p>
<p><img src="http://iubest.gitee.io/pic/redis-cache-service.png" alt="img"></p>
<h3 id="Django项目接入Redis"><a href="#Django项目接入Redis" class="headerlink" title="Django项目接入Redis"></a>Django项目接入Redis</h3><p>在此前的课程中，我们介绍过Redis的安装和使用，此处不再进行赘述。如果需要在Django项目中接入Redis，可以使用三方库<code>django-redis</code>，这个三方库又依赖了一个名为<code>redis</code> 的三方库，它封装了对Redis的各种操作。</p>
<p>安装<code>django-redis</code>。</p>
<figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">pip install django-redis</span><br></pre></td></tr></table></figure>

<p>修改Django配置文件中关于缓存的配置。</p>
<figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line">CACHES = &#123;</span><br><span class="line">    <span class="string">&#x27;default&#x27;</span>: &#123;</span><br><span class="line">        <span class="comment"># 指定通过django-redis接入Redis服务</span></span><br><span class="line">        <span class="string">&#x27;BACKEND&#x27;</span>: <span class="string">&#x27;django_redis.cache.RedisCache&#x27;</span>,</span><br><span class="line">        <span class="comment"># Redis服务器的URL</span></span><br><span class="line">        <span class="string">&#x27;LOCATION&#x27;</span>: [<span class="string">&#x27;redis://1.2.3.4:6379/0&#x27;</span>, ],</span><br><span class="line">        <span class="comment"># Redis中键的前缀（解决命名冲突）</span></span><br><span class="line">        <span class="string">&#x27;KEY_PREFIX&#x27;</span>: <span class="string">&#x27;vote&#x27;</span>,</span><br><span class="line">        <span class="comment"># 其他的配置选项</span></span><br><span class="line">        <span class="string">&#x27;OPTIONS&#x27;</span>: &#123;</span><br><span class="line">            <span class="string">&#x27;CLIENT_CLASS&#x27;</span>: <span class="string">&#x27;django_redis.client.DefaultClient&#x27;</span>,</span><br><span class="line">            <span class="comment"># 连接池（预置若干备用的Redis连接）参数</span></span><br><span class="line">            <span class="string">&#x27;CONNECTION_POOL_KWARGS&#x27;</span>: &#123;</span><br><span class="line">                <span class="comment"># 最大连接数</span></span><br><span class="line">                <span class="string">&#x27;max_connections&#x27;</span>: <span class="number">512</span>,</span><br><span class="line">            &#125;,</span><br><span class="line">            <span class="comment"># 连接Redis的用户口令</span></span><br><span class="line">            <span class="string">&#x27;PASSWORD&#x27;</span>: <span class="string">&#x27;foobared&#x27;</span>,</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;,</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>至此，我们的Django项目已经可以接入Redis，接下来我们修改项目代码，用Redis为之写的获取学科数据的接口提供缓存服务。</p>
<h3 id="为视图提供缓存服务"><a href="#为视图提供缓存服务" class="headerlink" title="为视图提供缓存服务"></a>为视图提供缓存服务</h3><h4 id="声明式缓存"><a href="#声明式缓存" class="headerlink" title="声明式缓存"></a>声明式缓存</h4><p>所谓声明式缓存是指不修改原来的代码，通过Python中的装饰器（代理）为原有的代码增加缓存功能。对于FBV，代码如下所示。</p>
<figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">from</span> django.views.decorators.cache <span class="keyword">import</span> cache_page</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="meta">@api_view((&#x27;GET&#x27;, ))</span></span><br><span class="line"><span class="meta">@cache_page(timeout=86400, cache=&#x27;default&#x27;)</span></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">show_subjects</span>(<span class="params">request</span>):</span></span><br><span class="line">    <span class="string">&quot;&quot;&quot;获取学科数据&quot;&quot;&quot;</span></span><br><span class="line">    queryset = Subject.objects.all()</span><br><span class="line">    data = SubjectSerializer(queryset, many=<span class="literal">True</span>).data</span><br><span class="line">    <span class="keyword">return</span> Response(&#123;<span class="string">&#x27;code&#x27;</span>: <span class="number">20000</span>, <span class="string">&#x27;subjects&#x27;</span>: data&#125;)</span><br></pre></td></tr></table></figure>

<p>上面的代码通过Django封装的<code>cache_page</code>装饰器缓存了视图函数的返回值（响应对象），<code>cache_page</code>的本意是缓存视图函数渲染的页面，对于返回JSON数据的视图函数，相当于是缓存了JSON数据。在使用<code>cache_page</code>装饰器时，可以传入<code>timeout</code>参数来指定缓存过期时间，还可以使用<code>cache</code>参数来指定需要使用哪一组缓存服务来缓存数据。Django项目允许在配置文件中配置多组缓存服务，上面的<code>cache=&#39;default&#39;</code>指定了使用默认的缓存服务（因为之前的配置文件中我们也只配置了名为<code>default</code>的缓存服务）。视图函数的返回值会被序列化成字节串放到Redis中（Redis中的str类型可以接收字节串），缓存数据的序列化和反序列化也不需要我们自己处理，因为<code>cache_page</code>装饰器会调用<code>django-redis</code>库中的<code>RedisCache</code>来对接Redis，该类使用了<code>DefaultClient</code>来连接Redis并使用了<a target="_blank" rel="noopener" href="https://python3-cookbook.readthedocs.io/zh_CN/latest/c05/p21_serializing_python_objects.html">pickle序列化</a>，<code>django_redis.serializers.pickle.PickleSerializer</code>是默认的序列化类。</p>
<p>如果缓存中没有学科的数据，那么通过接口访问学科数据时，我们的视图函数会通过执行<code>Subject.objects.all()</code>向数据库发出SQL语句来获得数据，视图函数的返回值会被缓存，因此下次请求该视图函数如果缓存没有过期，可以直接从缓存中获取视图函数的返回值，无需再次查询数据库。如果想了解缓存的使用情况，可以配置数据库日志或者使用Django-Debug-Toolbar来查看，第一次访问学科数据接口时会看到查询学科数据的SQL语句，再次获取学科数据时，不会再向数据库发出SQL语句，因为可以直接从缓存中获取数据。</p>
<p>对于CBV，可以利用Django中名为<code>method_decorator</code>的装饰器将<code>cache_page</code>这个装饰函数的装饰器放到类中的方法上，效果跟上面的代码是一样的。需要提醒大家注意的是，<code>cache_page</code>装饰器不能直接放在类上，因为它是装饰函数的装饰器，所以Django框架才提供了<code>method_decorator</code>来解决这个问题，很显然，<code>method_decorator</code>是一个装饰类的装饰器。</p>
<figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">from</span> django.utils.decorators <span class="keyword">import</span> method_decorator</span><br><span class="line"><span class="keyword">from</span> django.views.decorators.cache <span class="keyword">import</span> cache_page</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="meta">@method_decorator(decorator=cache_page(timeout=86400, cache=&#x27;default&#x27;), name=&#x27;get&#x27;)</span></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">SubjectView</span>(<span class="params">ListAPIView</span>):</span></span><br><span class="line">    <span class="string">&quot;&quot;&quot;获取学科数据的视图类&quot;&quot;&quot;</span></span><br><span class="line">    queryset = Subject.objects.all()</span><br><span class="line">    serializer_class = SubjectSerializer</span><br></pre></td></tr></table></figure>

<h4 id="编程式缓存"><a href="#编程式缓存" class="headerlink" title="编程式缓存"></a>编程式缓存</h4><p>所谓编程式缓存是指通过自己编写的代码来使用缓存服务，这种方式虽然代码量会稍微大一些，但是相较于声明式缓存，它对缓存的操作和使用更加灵活，在实际开发中使用得更多。下面的代码去掉了之前使用的<code>cache_page</code>装饰器，通过<code>django-redis</code>提供的<code>get_redis_connection</code>函数直接获取Redis连接来操作Redis。</p>
<figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">show_subjects</span>(<span class="params">request</span>):</span></span><br><span class="line">    <span class="string">&quot;&quot;&quot;获取学科数据&quot;&quot;&quot;</span></span><br><span class="line">    redis_cli = get_redis_connection()</span><br><span class="line">    <span class="comment"># 先尝试从缓存中获取学科数据</span></span><br><span class="line">    data = redis_cli.get(<span class="string">&#x27;vote:polls:subjects&#x27;</span>)</span><br><span class="line">    <span class="keyword">if</span> data:</span><br><span class="line">        <span class="comment"># 如果获取到学科数据就进行反序列化操作</span></span><br><span class="line">        data = json.loads(data)</span><br><span class="line">    <span class="keyword">else</span>:</span><br><span class="line">        <span class="comment"># 如果缓存中没有获取到学科数据就查询数据库</span></span><br><span class="line">        queryset = Subject.objects.all()</span><br><span class="line">        data = SubjectSerializer(queryset, many=<span class="literal">True</span>).data</span><br><span class="line">        <span class="comment"># 将查到的学科数据序列化后放到缓存中</span></span><br><span class="line">        redis_cli.set(<span class="string">&#x27;vote:polls:subjects&#x27;</span>, json.dumps(data), ex=<span class="number">86400</span>)</span><br><span class="line">    <span class="keyword">return</span> Response(&#123;<span class="string">&#x27;code&#x27;</span>: <span class="number">20000</span>, <span class="string">&#x27;subjects&#x27;</span>: data&#125;)</span><br></pre></td></tr></table></figure>

<p>需要说明的是，Django框架提供了<code>cache</code>和<code>caches</code>两个现成的变量来支持缓存操作，前者访问的是默认的缓存（名为<code>default</code>的缓存），后者可以通过索引运算获取指定的缓存服务（例如：<code>caches[&#39;default&#39;]</code>）。向<code>cache</code>对象发送<code>get</code>和<code>set</code>消息就可以实现对缓存的读和写操作，但是这种方式能做的操作有限，不如上面代码中使用的方式灵活。还有一个值得注意的地方，由于可以通过<code>get_redis_connection</code>函数获得的Redis连接对象向Redis发起各种操作，包括<code>FLUSHDB</code>、<code>SHUTDOWN</code>等危险的操作，所以在实际商业项目开发中，一般都会对<code>django-redis</code>再做一次封装，例如封装一个工具类，其中只提供了项目需要用到的缓存操作的方法，从而避免了直接使用<code>get_redis_connection</code>的潜在风险。当然，自己封装对缓存的操作还可以使用“Read Through”和“Write Through”的方式实现对缓存的更新，这个在下面会介绍到。</p>
<h3 id="缓存相关问题"><a href="#缓存相关问题" class="headerlink" title="缓存相关问题"></a>缓存相关问题</h3><h4 id="缓存数据的更新"><a href="#缓存数据的更新" class="headerlink" title="缓存数据的更新"></a>缓存数据的更新</h4><p>在使用缓存时，一个必须搞清楚的问题就是，当数据改变时，如何更新缓存中的数据。通常更新缓存有如下几种套路，分别是：</p>
<ol>
<li>Cache Aside Pattern</li>
<li>Read/Write Through Pattern</li>
<li>Write Behind Caching Pattern</li>
</ol>
<p>第1种方式的具体做法就是，当数据更新时，先更新数据库，再删除缓存。注意，不能够使用先更新数据库再更新缓存的方式，也不能够使用先删除缓存再更新数据库的方式，大家可以自己想一想为什么（考虑一下有并发的读操作和写操作的场景）。当然，先更新数据库再删除缓存的做法在理论上也存在风险，但是发生问题的概率是极低的，所以不少的项目都使用了这种方式。</p>
<p>第1种方式相当于编写业务代码的开发者要自己负责对两套存储系统（缓存和关系型数据库）的操作，代码写起来非常的繁琐。第2种方式的主旨是将后端的存储系统变成一套代码，对缓存的维护封装在这套代码中。其中，Read Through指在查询操作中更新缓存，也就是说，当缓存失效的时候，由缓存服务自己负责对数据的加载，从而对应用方是透明的；而Write Through是指在更新数据时，如果没有命中缓存，直接更新数据库，然后返回。如果命中了缓存，则更新缓存，然后再由缓存服务自己更新数据库（同步更新）。刚才我们说过，如果自己对项目中的Redis操作再做一次封装，就可以实现“Read Through”和“Write Through”模式，这样做虽然会增加工作量，但无疑是一件“一劳永逸”且“功在千秋”的事情。</p>
<p>第3种方式是在更新数据的时候，只更新缓存，不更新数据库，而缓存服务这边会<strong>异步的批量更新</strong>数据库。这种做法会大幅度提升性能，但代价是牺牲数据的<strong>强一致性</strong>。第3种方式的实现逻辑比较复杂，因为他需要追踪有哪数据是被更新了的，然后再批量的刷新到持久层上。</p>
<h4 id="缓存穿透"><a href="#缓存穿透" class="headerlink" title="缓存穿透"></a>缓存穿透</h4><p>缓存是为了缓解数据库压力而添加的一个中间层，如果恶意的访问者频繁的访问缓存中没有的数据，那么缓存就失去了存在的意义，瞬间所有请求的压力都落在了数据库上，这样会导致数据库承载着巨大的压力甚至连接异常，类似于分布式拒绝服务攻击（DDoS）的做法。解决缓存穿透的一个办法是约定如果查询返回为空值，把这个空值也缓存起来，但是需要为这个空值的缓存设置一个较短的超时时间，毕竟缓存这样的值就是对缓存空间的浪费。另一个解决缓存穿透的办法是使用布隆过滤器，具体的做法大家可以自行了解。</p>
<h4 id="缓存击穿"><a href="#缓存击穿" class="headerlink" title="缓存击穿"></a>缓存击穿</h4><p>在实际的项目中，可能存在某个缓存的key某个时间点过期，但恰好在这个时间点对有对该key的大量的并发请求过来，这些请求没有从缓存中找到key对应的数据，就会直接从数据库中获取数据并写回到缓存，这个时候大并发的请求可能会瞬间把数据库压垮，这种现象称为缓存击穿。比较常见的解决缓存击穿的办法是使用互斥锁，简单的说就是在缓存失效的时候，不是立即去数据库加载数据，而是先设置互斥锁（例如：Redis中的setnx），只有设置互斥锁的操作成功的请求，才能执行查询从数据库中加载数据并写入缓存，其他设置互斥锁失败的请求，可以先执行一个短暂的休眠，然后尝试重新从缓存中获取数据，如果缓存还没有数据，则重复刚才的设置互斥锁的操作，大致的参考代码如下所示。</p>
<figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">data = redis_cli.get(key)</span><br><span class="line"><span class="keyword">while</span> <span class="keyword">not</span> data:</span><br><span class="line">    <span class="keyword">if</span> redis_cli.setnx(<span class="string">&#x27;mutex&#x27;</span>, <span class="string">&#x27;x&#x27;</span>):</span><br><span class="line">        redis.expire(<span class="string">&#x27;mutex&#x27;</span>, timeout)</span><br><span class="line">        data = db.query(...)</span><br><span class="line">        redis.set(key, data)</span><br><span class="line">        redis.delete(<span class="string">&#x27;mutex&#x27;</span>)</span><br><span class="line">    <span class="keyword">else</span>:</span><br><span class="line">        time.sleep(<span class="number">0.1</span>)</span><br><span class="line">        data = redis_cli.get(key)</span><br></pre></td></tr></table></figure>

<h4 id="缓存雪崩"><a href="#缓存雪崩" class="headerlink" title="缓存雪崩"></a>缓存雪崩</h4><p>缓存雪崩是指在将数据放入缓存时采用了相同的过期时间，这样就导致缓存在某一时刻同时失效，请求全部转发到数据库，导致数据库瞬时压力过大而崩溃。解决缓存雪崩问题的方法也比较简单，可以在既定的缓存过期时间上加一个随机时间，这样可以从一定程度上避免不同的key在同一时间集体失效。还有一种办法就是使用多级缓存，每一级缓存的过期时间都不一样，这样的话即便某个级别的缓存集体失效，但是其他级别的缓存还能够提供数据，避免所有的请求都落到数据库上。供数据，避免所有的请求都落到数据库上。</p>
 
      <!-- reward -->
      
    </div>
    

    <!-- copyright -->
    
    <footer class="article-footer">
       
  <ul class="article-tag-list" itemprop="keywords"><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/django/" rel="tag">django</a></li></ul>

    </footer>
  </div>

    
 
   
</article>

    
  </article>
  

  
  <nav class="page-nav">
    
    <a class="extend prev" rel="prev" href="/page/2/">上一页</a><a class="page-number" href="/">1</a><a class="page-number" href="/page/2/">2</a><span class="page-number current">3</span><a class="page-number" href="/page/4/">4</a><a class="page-number" href="/page/5/">5</a><span class="space">&hellip;</span><a class="page-number" href="/page/14/">14</a><a class="extend next" rel="next" href="/page/4/">下一页</a>
  </nav>
  
</section>
</div>

      <footer class="footer">
  <div class="outer">
    <ul>
      <li>
        Copyrights &copy;
        2015-2020
        <i class="ri-heart-fill heart_icon"></i> TzWind
      </li>
    </ul>
    <ul>
      <li>
        
        
        
        由 <a href="https://hexo.io" target="_blank">Hexo</a> 强力驱动
        <span class="division">|</span>
        主题 - <a href="https://github.com/Shen-Yu/hexo-theme-ayer" target="_blank">Ayer</a>
        
      </li>
    </ul>
    <ul>
      <li>
        
        
        <span>
  <span><i class="ri-user-3-fill"></i>访问人数:<span id="busuanzi_value_site_uv"></span></s>
  <span class="division">|</span>
  <span><i class="ri-eye-fill"></i>浏览次数:<span id="busuanzi_value_page_pv"></span></span>
</span>
        
      </li>
    </ul>
    <ul>
      
    </ul>
    <ul>
      
    </ul>
    <ul>
      <li>
        <!-- cnzz统计 -->
        
        <script type="text/javascript" src='https://s9.cnzz.com/z_stat.php?id=1278069914&amp;web_id=1278069914'></script>
        
      </li>
    </ul>
  </div>
</footer>
      <div class="float_btns">
        <div class="totop" id="totop">
  <i class="ri-arrow-up-line"></i>
</div>

<div class="todark" id="todark">
  <i class="ri-moon-line"></i>
</div>

      </div>
    </main>
    <aside class="sidebar on">
      <button class="navbar-toggle"></button>
<nav class="navbar">
  
  <div class="logo">
    <a href="/"><img src="/images/ayer-side.svg" alt="Hexo"></a>
  </div>
  
  <ul class="nav nav-main">
    
    <li class="nav-item">
      <a class="nav-item-link" href="/">主页</a>
    </li>
    
    <li class="nav-item">
      <a class="nav-item-link" href="/archives">归档</a>
    </li>
    
    <li class="nav-item">
      <a class="nav-item-link" href="/categories">分类</a>
    </li>
    
    <li class="nav-item">
      <a class="nav-item-link" href="/tags">标签</a>
    </li>
    
    <li class="nav-item">
      <a class="nav-item-link" target="_blank" rel="noopener" href="http://www.baidu.com">百度</a>
    </li>
    
    <li class="nav-item">
      <a class="nav-item-link" href="/friends">友链</a>
    </li>
    
    <li class="nav-item">
      <a class="nav-item-link" href="/2019/about">关于我</a>
    </li>
    
  </ul>
</nav>
<nav class="navbar navbar-bottom">
  <ul class="nav">
    <li class="nav-item">
      
      <a class="nav-item-link nav-item-search"  title="搜索">
        <i class="ri-search-line"></i>
      </a>
      
      
      <a class="nav-item-link" target="_blank" href="/atom.xml" title="RSS Feed">
        <i class="ri-rss-line"></i>
      </a>
      
    </li>
  </ul>
</nav>
<div class="search-form-wrap">
  <div class="local-search local-search-plugin">
  <input type="search" id="local-search-input" class="local-search-input" placeholder="Search...">
  <div id="local-search-result" class="local-search-result"></div>
</div>
</div>
    </aside>
    <script>
      if (window.matchMedia("(max-width: 768px)").matches) {
        document.querySelector('.content').classList.remove('on');
        document.querySelector('.sidebar').classList.remove('on');
      }
    </script>
    <div id="mask"></div>

<!-- #reward -->
<div id="reward">
  <span class="close"><i class="ri-close-line"></i></span>
  <p class="reward-p"><i class="ri-cup-line"></i>请我喝杯咖啡吧~</p>
  <div class="reward-box">
    
    
  </div>
</div>
    
<script src="/js/jquery-2.0.3.min.js"></script>


<script src="/js/lazyload.min.js"></script>

<!-- Tocbot -->

<script src="https://cdn.jsdelivr.net/npm/jquery-modal@0.9.2/jquery.modal.min.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/jquery-modal@0.9.2/jquery.modal.min.css">
<script src="https://cdn.jsdelivr.net/npm/justifiedGallery@3.7.0/dist/js/jquery.justifiedGallery.min.js"></script>

<script src="/dist/main.js"></script>

<!-- ImageViewer -->

<!-- Root element of PhotoSwipe. Must have class pswp. -->
<div class="pswp" tabindex="-1" role="dialog" aria-hidden="true">

    <!-- Background of PhotoSwipe. 
         It's a separate element as animating opacity is faster than rgba(). -->
    <div class="pswp__bg"></div>

    <!-- Slides wrapper with overflow:hidden. -->
    <div class="pswp__scroll-wrap">

        <!-- Container that holds slides. 
            PhotoSwipe keeps only 3 of them in the DOM to save memory.
            Don't modify these 3 pswp__item elements, data is added later on. -->
        <div class="pswp__container">
            <div class="pswp__item"></div>
            <div class="pswp__item"></div>
            <div class="pswp__item"></div>
        </div>

        <!-- Default (PhotoSwipeUI_Default) interface on top of sliding area. Can be changed. -->
        <div class="pswp__ui pswp__ui--hidden">

            <div class="pswp__top-bar">

                <!--  Controls are self-explanatory. Order can be changed. -->

                <div class="pswp__counter"></div>

                <button class="pswp__button pswp__button--close" title="Close (Esc)"></button>

                <button class="pswp__button pswp__button--share" style="display:none" title="Share"></button>

                <button class="pswp__button pswp__button--fs" title="Toggle fullscreen"></button>

                <button class="pswp__button pswp__button--zoom" title="Zoom in/out"></button>

                <!-- Preloader demo http://codepen.io/dimsemenov/pen/yyBWoR -->
                <!-- element will get class pswp__preloader--active when preloader is running -->
                <div class="pswp__preloader">
                    <div class="pswp__preloader__icn">
                        <div class="pswp__preloader__cut">
                            <div class="pswp__preloader__donut"></div>
                        </div>
                    </div>
                </div>
            </div>

            <div class="pswp__share-modal pswp__share-modal--hidden pswp__single-tap">
                <div class="pswp__share-tooltip"></div>
            </div>

            <button class="pswp__button pswp__button--arrow--left" title="Previous (arrow left)">
            </button>

            <button class="pswp__button pswp__button--arrow--right" title="Next (arrow right)">
            </button>

            <div class="pswp__caption">
                <div class="pswp__caption__center"></div>
            </div>

        </div>

    </div>

</div>

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/photoswipe@4.1.3/dist/photoswipe.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/photoswipe@4.1.3/dist/default-skin/default-skin.min.css">
<script src="https://cdn.jsdelivr.net/npm/photoswipe@4.1.3/dist/photoswipe.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/photoswipe@4.1.3/dist/photoswipe-ui-default.min.js"></script>

<script>
    function viewer_init() {
        let pswpElement = document.querySelectorAll('.pswp')[0];
        let $imgArr = document.querySelectorAll(('.article-entry img:not(.reward-img)'))

        $imgArr.forEach(($em, i) => {
            $em.onclick = () => {
                // slider展开状态
                // todo: 这样不好，后面改成状态
                if (document.querySelector('.left-col.show')) return
                let items = []
                $imgArr.forEach(($em2, i2) => {
                    let img = $em2.getAttribute('data-idx', i2)
                    let src = $em2.getAttribute('data-target') || $em2.getAttribute('src')
                    let title = $em2.getAttribute('alt')
                    // 获得原图尺寸
                    const image = new Image()
                    image.src = src
                    items.push({
                        src: src,
                        w: image.width || $em2.width,
                        h: image.height || $em2.height,
                        title: title
                    })
                })
                var gallery = new PhotoSwipe(pswpElement, PhotoSwipeUI_Default, items, {
                    index: parseInt(i)
                });
                gallery.init()
            }
        })
    }
    viewer_init()
</script>

<!-- MathJax -->

<!-- Katex -->

<!-- busuanzi  -->


<script src="/js/busuanzi-2.3.pure.min.js"></script>


<!-- ClickLove -->

<!-- ClickBoom1 -->

<!-- ClickBoom2 -->

<!-- CodeCopy -->


<link rel="stylesheet" href="/css/clipboard.css">

<script src="https://cdn.jsdelivr.net/npm/clipboard@2/dist/clipboard.min.js"></script>
<script>
  function wait(callback, seconds) {
    var timelag = null;
    timelag = window.setTimeout(callback, seconds);
  }
  !function (e, t, a) {
    var initCopyCode = function(){
      var copyHtml = '';
      copyHtml += '<button class="btn-copy" data-clipboard-snippet="">';
      copyHtml += '<i class="ri-file-copy-2-line"></i><span>COPY</span>';
      copyHtml += '</button>';
      $(".highlight .code pre").before(copyHtml);
      $(".article pre code").before(copyHtml);
      var clipboard = new ClipboardJS('.btn-copy', {
        target: function(trigger) {
          return trigger.nextElementSibling;
        }
      });
      clipboard.on('success', function(e) {
        let $btn = $(e.trigger);
        $btn.addClass('copied');
        let $icon = $($btn.find('i'));
        $icon.removeClass('ri-file-copy-2-line');
        $icon.addClass('ri-checkbox-circle-line');
        let $span = $($btn.find('span'));
        $span[0].innerText = 'COPIED';
        
        wait(function () { // 等待两秒钟后恢复
          $icon.removeClass('ri-checkbox-circle-line');
          $icon.addClass('ri-file-copy-2-line');
          $span[0].innerText = 'COPY';
        }, 2000);
      });
      clipboard.on('error', function(e) {
        e.clearSelection();
        let $btn = $(e.trigger);
        $btn.addClass('copy-failed');
        let $icon = $($btn.find('i'));
        $icon.removeClass('ri-file-copy-2-line');
        $icon.addClass('ri-time-line');
        let $span = $($btn.find('span'));
        $span[0].innerText = 'COPY FAILED';
        
        wait(function () { // 等待两秒钟后恢复
          $icon.removeClass('ri-time-line');
          $icon.addClass('ri-file-copy-2-line');
          $span[0].innerText = 'COPY';
        }, 2000);
      });
    }
    initCopyCode();
  }(window, document);
</script>


<!-- CanvasBackground -->


    
  </div>
</body>

</html>